javascript - Sinon - 如何替换我想测试的方法中调用的方法
我在 TypeScript 中遇到麻烦,无法模拟一个被我想要测试的方法使用的方法。我在示例中为了清晰起见删掉了很多方法本身的内容,但基本上我有一个 getServiceWithRetry 方法,它会调用 getService 方法。
即
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 promises 的人来说,.then(function(value){}, function(error){}) 方法处理 promise 成功时和被拒绝时会发生的情况。
答案#1
你需要更改:
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()。
答案#2
问题在于,使用 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)放在一个类里,并使用依赖注入。你会创建一个类,在其构造函数中接收依赖(getService 方法),在内部存储它,并在调用生成对象的方法时使用它。在你的生产代码中,需要让其他东西正确地把这些组合在一起,然后在你的测试中,你可以传入一个存根对象。
我预计在这种情况下也有点过头,但你可以把
getService拿到一个 Lookup 导入的完全独立的模块中,并使用类似 Rewire 的工具在测试期间将其替换为不同的模块。这实际上与依赖注入选项非常相似,并且使你的生产代码更简单,但代价是使你的测试代码更复杂和神秘。
