网站搜索

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 的工具在测试期间将其替换为不同的模块。

    这实际上与依赖注入选项非常相似,并且使你的生产代码更简单,但代价是使你的测试代码更复杂和神秘。