7

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).

peterstarling
  • 533
  • 4
  • 12

2 Answers2

4

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.

paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • 1
    So DI (of function dependencies) in FP is actually required as soon as you have to deal with impure functions. You can then reduce explicit argument passing with currying/partial application. Thus, currying can be considered as functional dependency injection. –  Aug 11 '17 at 19:58
  • This is a good answer. I was indeed also going to make the two functions dependencies. Could you elaborate a bit more on that bit, though: "if you were able to make them pure functions then by definition anything composed of them would also be a pure function"? – peterstarling Aug 11 '17 at 20:45
  • @peterstarling updated. Apologies I didn't read your comment correctly at first so I digressed a bit about functional purity. I decided to leave that bit for future readers, but the answer to your question is in there. – paulpdaniels Aug 11 '17 at 22:03
  • @paulpdaniels that's even clearer now, however one thing that interests me is that bit "you can test them independently to verify that they work as expected, but because they are pure, their composition (...) will remain pure as well". As the compositions can vary (order and the way they are put together does matter, see addThenMul and addThenSquare both using same pure functions but resulting in different composition) shouldn't we test it separately? – peterstarling Aug 14 '17 at 12:41
  • @peterstarling yes you would still test the combinations separately, to verify that the composition is still correct. However, you don't have to mock them because the components are pure. So there aren't any side effects to worry about. – paulpdaniels Aug 15 '17 at 05:57
0

Have you considered looking into any of these mocking packages? In particular rewire might be suitable.

Proxyquire, rewire, SandboxedModule, and Sinon: pros & cons

markrian
  • 1,027
  • 10
  • 16
  • I am aware of their existence, even used proxyquire in the past, but for the reason stated in the answer (feels like cheating) I decided not to go that way. Surely there is a right way to make function composition testable (again, if what I posted actually is one). – peterstarling Aug 11 '17 at 14:03