【问题标题】:How Sinon stubs works under the hood?Sinon 存根如何在幕后工作?
【发布时间】:2017-12-09 11:28:59
【问题描述】:

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

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

过去几年我最常使用的语言是 Java。在 Java 中,我使用 Mockito 来模拟/存根依赖项和依赖项注入。我曾经导入类,用@Mock 注释字段并将这个模拟作为参数传递给被测类。我很容易看出我在做什么:模拟一个类并将模拟作为参数传递。

当我第一次开始使用 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
    })
})

moduleUnderTest.js

const request = require('request')
// some code
  request
    .get("some_url", requestParams, onResponse)

而且它有效。当我们运行测试时,实现中的request moduleUnderTest.js 调用了request 模块的存根版本。

我的问题是:为什么会这样?

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

我尝试遵循 Sinon repo 中 stub.js 代码中的逻辑,但我对 JavaScript 还不是很熟悉。 对不起,很长的帖子,如果这是一个愚蠢的问题,对不起。 :)

【问题讨论】:

    标签: javascript unit-testing mocking sinon stub


    【解决方案1】:

    如果我们不将存根对象作为参数传递(注入),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);
    

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

    require('request') 每次调用都会返回相同的(对象)引用,该引用是在“请求”模块中创建的。

    NodeJS documentation

    模块在第一次加载后被缓存。这意味着(除其他外)对require('foo') 的每次调用都将返回完全相同的对象,前提是它会解析为同一个文件。

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

    如果还不清楚:它只替换从“请求”模块返回的对象引用的单个方法,它不会替换模块。

    这就是你不打电话的原因

    stub(obj.method)
    

    因为这只会传递对函数method 的引用。诗浓将无法更改对象obj

    文档进一步说:

    如果你想让一个模块多次执行代码,那么导出一个函数,然后调用那个函数。

    也就是说,如果一个模块看起来像这样:

    foo.js

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

    ma​​in.js

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

    这样的模块工厂函数不能被存根,因为你不能替换 require() 从模块中导出的内容。


    在 Java 中,我使用 Mockito 来模拟/存根依赖项和依赖项注入。我曾经导入类,用@Mock 注释字段并将这个模拟作为参数传递给被测类。我很容易看出我在做什么:模拟一个类并将模拟作为参数传递。

    在 Java 中,您(没有)无法将方法重新分配给新值,这是无法做到的。相反,会生成新的字节码,使模拟提供与模拟类相同的接口。与Sinon相比,Mockito所有方法都是mock的,应该是explictly instructed才能调用真正的方法。

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

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

    @Mock
    Type field;
    

    Type field = mock(Type.class)
    

    其实相当于Sinonsmocks

    var myAPI = { method: function () {} };
    var mock = sinon.mock(myAPI);
    
    mock.expects("method").once().throws();
    

    方法是先replaced with the expects() call:

    wrapMethod(this.object, method, function () {
        return mockObject.invokeMethod(method, this, arguments);
    });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-05
      • 2015-11-28
      • 2018-10-15
      • 2017-05-14
      • 2020-11-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多