功能性javascript和RxJS中的依赖注入和模拟

时间:2017-08-11 13:33:30

标签: javascript unit-testing dependency-injection functional-programming rxjs

我正在尝试使用RxJS和函数组合将使用经典OO Javascript编写的库重写为更具功能性和反应性的方法。我已经开始使用以下两个易于测试的函数(我跳过了Observables的导入):

创建-connection.js

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

创建-channel.js

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

我需要做的就是测试它们是注入amqplib或连接的模拟,并确保调用正确的方法,如下所示:

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');
    });
});

所以现在我想将这两个函数放在一起:

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

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

然而,虽然这限制了我在测试方面的选择,因为我无法注入amqplib的模拟。我也许可以将它作为依赖项添加到我的函数参数中,但这样我就必须在树中遍历并传递依赖关系,如果任何其他组合将要使用它。此外,我希望能够模拟createConnectioncreateChannel函数,甚至不必测试我之前测试的相同行为,我是说我还必须将它们作为依赖项添加?

如果是这样的话,我可以在构造函数中有一个带有依赖项的工厂函数/类,然后使用某种形式的控制反转来管理它们并在必要时注入它们,但这实际上让我回到了我开始的地方,这是面向对象的方法

我知道我可能做错了什么,但老实说我找到了零(null,nada)关于测试具有函数组合的函数javascript的教程(除非这不是一个,在这种情况下是什么)。< / p>

2 个答案:

答案 0 :(得分:4)

RxJS in Action的第9章免费提供here,如果您想要更深入的阅读(完全披露:我是作者之一),请完全涵盖该主题。

&安培; tldr;函数式编程鼓励透明的参数传递。因此,当您朝着使应用程序更具组合性迈出了一大步时,您可以更进一步,确保将副作用推向应用程序的外部。

这在实践中如何看待? Javascript中的一个有趣模式是函数currying,它允许您创建映射到其他函数的函数。因此,对于您的示例,我们可以将amqlib注入转换为参数:

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

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

现在你会像这样使用它:

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)

您可以更进一步,并注入其他依赖项createConnectioncreateChannel。虽然如果你能够使它们成为纯函数,那么根据定义,由它们组成的任何东西也都是纯粹的函数。

这是什么意思?

如果我给你两个功能:

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

或概括为curried函数:

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

addmulti都被认为是函数,也就是说,给定相同的输入集会产生相同的输出(读取:没有边 - 效果)。你也会听到这被称为参考透明度(值得一个谷歌)。

鉴于上述两个函数是 pure ,我们可以进一步断言这些函数的任何组合也是纯粹的,即

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

即使没有正式的证据,这至少应该是直观明确的,只要没有子组件添加副作用,那么整个组件就不应该有副作用。

由于它与您的问题有关,createConnectioncreateChannel是纯粹的,因此实际上并不需要模仿它们,因为它们的行为是在功能上驱动的(而不是内部状态驱动的) 。您可以单独测试它们以验证它们是否按预期工作,但由于它们是纯净的,它们的组成(即createConnection(amqlib, host).mergeMap(createChannel))也将保持纯净。

加成

此属性也存在于Observables中。也就是说,两个纯Observable的组合将永远是另一个纯Observable。

答案 1 :(得分:0)

您是否考虑过调查任何这些嘲弄套餐?特别是rewire可能是合适的。

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