Sinon - 如何对我要测试的方法调用的方法进行存根

Sinon - How to stub a method called by the method I want to test

我在对我想在打字稿中测试的方法所使用的方法进行存根时遇到问题。为了清楚起见,我在示例中删除了很多方法本身,但基本上我有一个调用 getService 方法的 getServiceWithRetry 方法。

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    getService(name)
    //do more stuff
}

export function getService(name:string) {
    //lookup stuff
}

这作为 Lookup 导入到我的测试中。如果我在测试中调用 getService,我可以成功地存根 getService 方法,但是当我 运行 getServiceWithRetry 它调用实际的 getService 方法而不是存根。有谁知道我做错了什么?

it("test", function(done) {
    let serviceStub = sinon.stub(Lookup, 'getService')

    serviceStub.returns(Promise.resolve("resolved"))

    //this uses the stub
    Lookup.getService("name").then(function(value) {
        console.log("success: "+value)
    }, function(error) {
        console.log("error: "+error)
    })

    //this calls the actual method, not the stub as I would expect it to
    Lookup.getServiceWithRetry("serviceName", 4).then(function(value) {
        console.log("success: "+value)
    }, function(error) {
        console.log("error: "+error)
    })
    done()
})

注意:对于那些不熟悉 bluebird 承诺的人,.then(function(value){}, function(error){}) 方法处理承诺成功和拒绝承诺时发生的情况。

问题在于,使用 sinon.stub(Lookup, 'getService') 时,您正在改变测试中持有的 Lookup 变量的内部,然后从该变量获取方法。在您的 Lookup 模块中,该函数只是直接从其本地范围查找 getService 。在外部,我不认为有任何方法可以扰乱该范围,所以恐怕没有简单的魔术修复。

通常,您通常无法在测试中很好地模拟单个模块的某些部分。你需要稍微重组一下,有几个选项:

  • 完全分开测试。将 getServiceWithRetry 更改为通用 retry 方法,例如所以你可以称它为 retry(nTimes, getService, "serviceName")retry(() => getService("serviceName"), nTimes))。如果这样做可行(即,如果它与 getService 的关系不太紧密),那么您可以轻松地自行测试它:

    var myStub = sinon.stub();
    
    myStub.onCall(0).throw("fail once");
    myStub.onCall(0).throw("fail twice");
    myStub.returns(true); // then return happily
    
    expect(retry(myStub, 1)).to.throw("fail twice"); // gives up after one retry
    expect(retry(myStub, 5)).to.return(true); // keeps going to success
    

    如果您希望在其他地方只调用一个 getServiceWithRetry,您可以轻松构建一个:var getServiceWithRetry = (arg, triesLeft) => retry(getService, tries)

  • 放弃,一起测试。这意味着将 getService 依赖的东西存根,而不是直接存根。这取决于您希望测试的粒度级别,但如果这段代码很简单并且您可以进行更粗略的测试,那么这可能是一个简单的选择。

    即使您已经将它们分开,您也可能想要执行此操作,以获得单元和集成测试以获得额外的覆盖率。如果它们之间发生了一些更复杂的交互,则情况更是如此。

  • 据我所知,在这种情况下可能不相关,但在其他情况下,有点像将待测方法 (getServiceWithRetry) 放在 class 中,并使用依赖注入。您将创建一个 class,它在其构造函数中获取依赖项(getService 方法),将其存储在内部,并在稍后对结果对象调用方法时使用它。在您的生产代码中,其他东西必须将它们正确地粘合在一起,然后在您的测试中,您可以传入一个存根。

  • 我预计对于这种情况也有点矫枉过正,但您可以将 getService 拉入 Lookup 导入的完全独立的模块,并使用 Rewire 之类的东西将其换出测试期间的不同模块。

    这与依赖注入选项非常相似,使您的生产代码更简单,但代价是使您的测试代码更加复杂和神奇。

您需要更改:

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    getService(name)
    //do more stuff
}

至:

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    this.getService(name)
    //do more stuff
}

这样当您调用 Lookup.getServiceWithRetry() 时,getService() 调用将指向 Lookup.getService() 而不是驻留在您要导出的模块中的 getService()

由于您使用的是 TypeScript,因此使用 ts-mockito (npm install --save ts-mockquito) 可能会更好。

ts-mockito 支持类型。

然后您可以模拟您的 类,例如(来自 README,稍作修改):

// Creating mock
let mockedFoo:Foo = mock(Foo);

// Getting instance from mock
let foo:Foo = instance(mockedFoo);

// Using instance in source code
foo.getBar(3);
foo.getBar(5);

// Explicit, readable verification
verify(mockedFoo.getBar(3)).called();
verify(mockedFoo.getBar(5)).called();
when(mockedFoo.getBar(4)).thenReturn('three');