Sinon 允许您轻松地存根对象的单个实例方法。当然,由于b 是一个单例,您需要在每次测试后回滚它,以及您可能对b 进行的任何其他更改。如果你不这样做,呼叫计数和其他状态将从一个测试泄漏到另一个测试。如果这种全局状态处理不当,您的套件可能会变成一堆地狱般的测试,具体取决于其他测试。
重新排序一些测试?以前没有的东西失败了。
添加、更改或删除测试?一堆其他测试现在失败了。
尝试运行单个测试或测试子集?他们现在可能会失败。或者更糟糕的是,它们在您编写或编辑它们时孤立地通过,但在整个套件运行时失败。
相信我,这很糟糕。
因此,按照此建议,您的测试可能如下所示:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const b = require('./b');
describe('A', function() {
describe('#doSomething', function() {
beforeEach(function() {
sinon.stub(b, 'doSomething').resolves();
});
afterEach(function() {
b.doSomething.restore();
});
it('does something', function() {
let a = new A(b);
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
但是,这并不是我所推荐的。
我通常会尽量避免教条式的建议,但这只是少数例外之一。如果您正在进行单元测试、TDD 或 BDD,则通常应避免使用单例。它们不能很好地与这些做法混合,因为它们使测试后的清理变得更加困难。在上面的例子中,这很简单,但是随着B 类添加了越来越多的功能,清理工作变得越来越繁重且容易出错。
那么你会怎么做呢?让您的 B 模块导出 B 类。如果您想保持您的 DI 模式并避免在 A 模块中需要 B 模块,您只需在每次创建 A 实例时创建一个新的 B 实例。
按照此建议,您的测试可能如下所示:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = new B();
let a = new A(b);
sinon.stub(b, 'doSomething').resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
您会注意到,由于每次都会重新创建 B 实例,因此不再需要恢复存根的 doSomething 方法。
Sinon 还有一个名为createStubInstance 的简洁实用函数,它允许您在测试期间完全避免调用B 构造函数。它基本上只是为任何原型方法创建一个带有存根的空对象:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = sinon.createStubInstance(B);
let a = new A(b);
b.doSomething.resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
最后,最后一点与问题没有直接关系的建议——Promise 构造函数永远不应该用于包装承诺。这样做是多余且令人困惑的,并且违背了 Promise 的目的,即使异步代码更易于编写。
Promise.prototype.then 方法带有内置的有用行为,因此您永远不必执行这种多余的包装。调用then 总是返回一个promise(我将在下文中将其称为'chained promise'),其状态将取决于处理程序:
- 返回非承诺值的
then 处理程序将导致链式承诺以该值解析。
-
then 抛出的处理程序将导致链式 Promise 以抛出的值拒绝。
- 返回 promise 的
then 处理程序将导致链式 promise 与返回的 promise 的状态相匹配。因此,如果它使用一个值解析或拒绝,则链式 Promise 将使用相同的值解析或拒绝。
所以你的A 类可以像这样大大简化:
class A {
constructor(b) {
this.b = b;
}
doSomething(id) {
return this.b.doOther()
.then(() =>{
// various things that will return or throw
});
}
}
module.exports = A;