Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency injection and mocking in functional javascript and RxJS

I am trying to rewrite a library written in classical OO Javascript into a more functional and reactive approach using RxJS and function composition. I have begun with following two, easily testable functions (I skipped import of Observables):

create-connection.js

export default (amqplib, host) => Observable.fromPromise(amqplib.connect(host))

create-channel.js

export default connection => Observable.fromPromise(connection.createChannel())

All I have to do to test them is to inject a mock of amqplib or connection and make sure right methods are being called, like so:

import createChannel from 'create-channel';

test('it should create channel', t => {
    const connection = { createChannel: () => {}};
    const connectionMock = sinon.mock(connection);

    connectionMock.expects('createChannel')
        .once()
        .resolves('test channel');

    return createChannel(connection).map(channel => {
        connectionMock.verify();
        t.is(channel, 'test channel');
    });
});

So now I would like to put the two functions together like so:

import amqplib from 'amqplib';
import { createConnection, createChannel } from './';

export default ({ host }) => 
    createConnection(amqlib, host)
        .mergeMap(createChannel)

However though this limits my options when it comes to testing because I cannot inject a mock of amqplib. I could perhaps add it to my function arguments as a dependency but that way I would have to traverse all the way up in a tree and pass dependencies around if any other composition is going to use it. Also I would like to be able to mock createConnection and createChannel functions without even having to test the same behaviour I tested before, would that I mean I would have to add them as dependencies too?

If so I could have a factory function/class with dependencies in my constructor and then use some form of inversion of control to manage them and inject them when necessary, however that essentially puts me back where I started which is Object Oriented approach.

I understand I am probably doing something wrong, but to be honest I found zero (null, nada) tutorials about testing functional javascript with function composition (unless this isn't one, in which case what is).

like image 960
peterstarling Avatar asked Aug 11 '17 13:08

peterstarling


People also ask

Is mocking a dependency injection?

Dependency injection is a way to scale the mocking approach. If a lot of use cases are relying on the interaction you'd like to mock, then it makes sense to invest in dependency injection. Systems that lend themselves easily to dependency injection: An authentication/authorization service.

Why mocking is a code smell?

A code smell does not mean that something is definitely wrong, or that something must be fixed right away. It is a rule of thumb that should alert you to a possible opportunity to improve something. This text and its title in no way imply that all mocking is bad, or that you should never mock anything.

Why is dependency injection useful?

The dependency injection technique enables you to improve this even further. It provides a way to separate the creation of an object from its usage. By doing that, you can replace a dependency without changing any code and it also reduces the boilerplate code in your business logic.

What is the dependency injection?

In software engineering, dependency injection is a design pattern in which an object or function receives other objects or functions that it depends on. A form of inversion of control, dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs.


1 Answers

Chapter 9 of RxJS in Action is available for free here and covers the topic pretty thoroughly if you want a more in depth read (full disclosure: I am one of the authors).

&tldr; Functional programming encourages transparent argument passing. So while you took a good step toward making your application more composable you can go even further by making sure to push your side-effects to the outside of your application.

How does this look in practice? Well one fun pattern in Javascript is function currying which allows you to create functions that map to other functions. So for your example we could convert the amqlib injection into an argument instead:

import { createConnection, createChannel } from './';

export default (amqlib) => ({ host }) => 
    createConnection(amqlib, host)
        .mergeMap(createChannel);

Now you would use it like so:

import builder from './amqlib-adapter'
import amqlib from 'amqlib'

// Now you can pass around channelFactory and use it as you normally would
// you replace amqlib with a mock dependency when you test it.
const channelFactory = builder(amqlib)

You could take it a step further and also inject the other dependencies createConnection and createChannel. Though if you were able to make them pure functions then by definition anything composed of them would also be a pure function.

What does this mean?

If I give you two functions:

const add => (a, b) => a + b;
const mul => (a, b) => a * b;

Or generalized as curried functions:

const curriedAdd => (a) => (b) => a + b;
const curriedMul => (a) => (b) => a * b;

Both add and multi are considered pure functions, that is, given the same set of inputs will result in the same output (read: there are no side-effects). You will also hear this referred to as referential transparency (it's worth a google).

Given that the two functions above are pure we can further assert that any composition of those functions will also be pure, i.e.

const addThenMul = (a, b, c) => mul(add(a, b), c);
const addThenSquare = (a, b) => { const c = add(a, b); return mul(c, c); }

Even without a formal proof this should be at least intuitively clear, as long as none of the sub-components add side effects, then the component as a whole should not have side-effects.

As it relates to your issue, createConnection and createChannel are pure, then there isn't actually a need to mock them, since their behavior is functionally driven (as opposed to internally state driven). You can test them independently to verify that they work as expected, but because they are pure, their composition (i.e. createConnection(amqlib, host).mergeMap(createChannel)) will remain pure as well.

Bonus

This property exists in Observables as well. That is, the composition of two pure Observables will always be another pure Observable.

like image 98
paulpdaniels Avatar answered Oct 06 '22 06:10

paulpdaniels