Sinon存根如何在引擎盖下工作?

Luc*_*ier 6 javascript unit-testing mocking stub sinon

在过去的几个月里,我一直在使用JavaScript并使用SinonJS来存储某些行为.我已经设法使它工作,我已经存在许多方法,一切都很好.

但我仍然对Sinon如何在桌面下工作有一些疑问.我想我说的是Sinon,但是这个问题可能适用于所有其他用于模拟/存根/间谍的库.

我过去几年最常用的语言是Java.在Java中,我使用Mockito来模拟/存根依赖项和依赖项注入.我以前导入了Class,用@Mock该元素注释并将这个模拟作为param传递给被测试的类.我很容易看到我在做什么:嘲笑一个类并将模拟作为参数传递.

当我第一次开始使用SinonJS时,我看到了这样的事情:

moduleUnderTest.spec.js

const request = require('request')

describe('Some tests', () => {
  let requestStub

  beforeEach(() => {
    requestStub = sinon.stub(request, 'get')
  })

  afterEach(() => {
    request.get.restore()
  })

  it('A test case', (done) => {
    const err = undefined
    const res = { statusCode: 200 }
    const body = undefined
    requestStub
      .withArgs("some_url")
      .yields(err, res, body)

    const moduleUnderTest = moduleUnderTest.someFunction()

    // some assertions
    })
})
Run Code Online (Sandbox Code Playgroud)

moduleUnderTest.js

const request = require('request')
// some code
  request
    .get("some_url", requestParams, onResponse)
Run Code Online (Sandbox Code Playgroud)

它有效.当我们运行测试时,实现request内部moduleUnderTest.js调用request模块的存根版本.

我的问题是:为什么这有效?

当测试调用实现执行时,实现需要并使用该request模块.如果我们没有将存根对象作为param(注入它)传递,Sinon(以及其他模拟/存根/间谍库)如何设法使实现调用存根?Sinon request在测试执行期间替换整个模块(或部分模块),使存根可用require('request'),然后在测试完成后恢复它?

我试图遵循stub.jsSinon repo 中的代码逻辑,但我还不熟悉JavaScript.对不起,这篇文章很长,很遗憾.:)

try*_*lly 10

如果我们没有将存根对象作为param(注入它)传递,Sinon(以及其他模拟/存根/间谍库)如何设法使实现调用存根?

让我们编写自己的简单存根工具,不是吗?

为简洁起见,它非常有限,不提供存根API,每次只返回42.但这足以说明诗乃是如何运作的.

function stub(obj, methodName) {
    // Get a reference to the original method by accessing
    // the property in obj named by methodName.
    var originalMethod = obj[methodName];

    // This is actually called on obj.methodName();
    function replacement() {
        // Always returns this value
        return 42;

        // Note that in this scope, we are able to call the
        // orignal method too, so that we'd be able to 
        // provide callThrough();
    }

    // Remember reference to the original method to be able 
    // to unstub (this is *one*, actually a little bit dirty 
    // way to reference the original function)
    replacement.originalMethod = originalMethod;

    // Assign the property named by methodName to obj to 
    // replace the method with the stub replacement
    obj[methodName] = replacement;

    return {
        // Provide the stub API here
    };
}

// We want to stub bar() away
var foo = {
    bar: function(x) { return x * 2; }
};

function underTest(x) {
    return foo.bar(x);
}

stub(foo, "bar");
// bar is now the function "replacement"
// foo.bar.originalMethod references the original method

underTest(3);
Run Code Online (Sandbox Code Playgroud)

Sinon module在测试执行期间替换整个请求(或部分请求),使存根可用require('request'),然后在测试完成后恢复它?

require('request') 每次调用时,都会返回在"request"模块中创建的相同(对象)引用.

请参阅NodeJS文档:

模块在第一次加载后进行缓存.这意味着(除其他外)每个调用require('foo')将返回完全相同的对象,如果它将解析为同一个文件.

多次调用require('foo')可能不会导致模块代码多次执行.这是一个重要的特征.有了它,就可以返回"部分完成"的对象,从而允许加载传递依赖,即使它们会导致循环.

如果它还没有变得清晰:它只替换从"请求"模块返回的对象引用的单个方法,它不替换模块.

这就是你不打电话的原因

stub(obj.method)
Run Code Online (Sandbox Code Playgroud)

因为这只会传递对函数的引用method.诗乃无法改变物体obj.

文件进一步说:

如果要让模块多次执行代码,则导出一个函数,然后调用该函数.

这意味着,如果模块看起来像这样:

foo.js

module.exports = function() {
    return {
        // New object everytime the required "factory" is called
    };
};
Run Code Online (Sandbox Code Playgroud)

main.js

        // The function returned by require("foo") does not change
const   moduleFactory = require("./foo"),
        // This will change on every call
        newFooEveryTime = moduleFactory();
Run Code Online (Sandbox Code Playgroud)

此类模块工厂函数无法存根,因为您无法替换require() 模块中导出的内容.


在Java中,我使用Mockito来模拟/存根依赖项和依赖项注入.我以前导入了Class,用@Mock该元素注释并将这个模拟作为param传递给被测试的类.我很容易看到我在做什么:嘲笑一个类并将模拟作为参数传递.

在Java中,您(无)无法将方法重新分配给新值,这是不可能的.而是生成新的字节码,使模拟提供与模拟类相同的接口.与Sinon相比,Mockito所有的方法都被嘲笑,并且应该明确地指示调用真正的方法.

Mockito将有效地调用mock()并最终将结果分配给带注释的字段.

但是您仍然需要将mock替换/分配给被测试类中的字段,或者将其传递给测试方法,因为该模拟本身没有帮助.

@Mock
Type field;
Run Code Online (Sandbox Code Playgroud)

要么

Type field = mock(Type.class)
Run Code Online (Sandbox Code Playgroud)

实际上相当于Sinons 嘲笑

var myAPI = { method: function () {} };
var mock = sinon.mock(myAPI);

mock.expects("method").once().throws();
Run Code Online (Sandbox Code Playgroud)

该方法首先expects()调用替换:

wrapMethod(this.object, method, function () {
    return mockObject.invokeMethod(method, this, arguments);
});
Run Code Online (Sandbox Code Playgroud)