Javascript Sinon-如何存根我要测试的方法调用的方法

Javascript Sinon-如何存根我要测试的方法调用的方法,javascript,typescript,sinon,Javascript,Typescript,Sinon,我在存根一个我想在typescript中测试的方法所使用的方法时遇到问题。为了清晰起见,我在示例中去掉了很多方法本身,但基本上我有一个调用getService方法的getServiceWithRetry方法 即 这将作为Lookup导入到我的测试中。如果我在测试中调用getService,我可以成功地删除getService方法,但是当我运行getServiceWithRetry时,它调用实际的getService方法,而不是存根。有人知道我做错了什么吗 it("test", function(

我在存根一个我想在typescript中测试的方法所使用的方法时遇到问题。为了清晰起见,我在示例中去掉了很多方法本身,但基本上我有一个调用
getService
方法的
getServiceWithRetry
方法

这将作为
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承诺的
。然后(函数(值){},函数(错误){})
方法处理承诺成功和承诺被拒绝时发生的情况。

问题在于
sinon.stub(查找,'getService'))
您正在变异测试中保存的查找变量的内部,然后从该变量获取方法。在查找模块中,函数只是直接从其本地范围查找
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),并使用依赖项注入。您将创建一个类,该类在其构造函数中接受依赖项(getService方法),在内部存储该依赖项,并在以后调用结果对象上的方法时使用它。在您的生产代码中,其他一些东西必须正确地将它们粘合在一起,然后在您的测试中,您可以传递一个存根

  • 对于这种情况,我认为也有点过头了,但是您可以将
    getService
    拉入一个完全独立的模块中进行查找导入,并在测试过程中使用类似的方法将其交换到另一个模块中

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

您需要更改:

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


这样,当您调用
Lookup.getServiceWithRetry()
时,
getService()
调用将指向
Lookup.getService()
,而不是位于导出源模块中的
getService()

由于您使用的是TypeScript,所以最好使用(
npm安装--保存ts mockquito

ts mockito
支持类型

然后,您可以模拟您的类,如(从自述文件中,稍加修改):

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