如何使用sinon正确模拟ES6类

Jar*_*ede 3 javascript bdd mocha.js node.js sinon

我希望能够正确测试我的ES6类,它的构造函数需要另一个类,所有这些看起来像这样:

A级

class A {
  constructor(b) {
    this.b = b;
  }

  doSomething(id) {
    return new Promise( (resolve, reject) => {
      this.b.doOther()
        .then( () => {
          // various things that will resolve or reject
        });
    });
  }
}
module.exports = A;
Run Code Online (Sandbox Code Playgroud)

B级

class B {
  constructor() {}

  doOther() {
    return new Promise( (resolve, reject) => {
      // various things that will resolve or reject
    });
}
module.exports = new B();
Run Code Online (Sandbox Code Playgroud)

指数

const A = require('A');
const b = require('b');

const a = new A(b);
a.doSomething(123)
  .then(() => {
    // things
  });
Run Code Online (Sandbox Code Playgroud)

由于我试图进行依赖注入而不是在类的顶部进行需求注入,因此我不确定如何模拟类B及其用于测试类A的功能。

Pat*_*tal 5

我认为您正在寻找proxyquire库。

为了演示这一点,我对您的文件进行了一些编辑,以直接将 b 包含在 a 中(我这样做是因为您的 singleton new B,但是您可以保留您的代码,这样更容易理解 proxyquire 。

b.js

class B {
  constructor() {}
  doOther(number) {
    return new Promise(resolve => resolve(`B${number}`));
  }
}

module.exports = new B();
Run Code Online (Sandbox Code Playgroud)

a.js

const b = require('./b');

class A {
  testThis(number) {
    return b.doOther(number)
      .then(result => `res for ${number} is ${result}`);
  }
}

module.exports = A;
Run Code Online (Sandbox Code Playgroud)

我现在想a.js通过模拟 b 的行为来进行测试。在这里你可以这样做:

const proxyquire = require('proxyquire');
const expect = require('chai').expect;

describe('Test A', () => {
  it('should resolve with B', async() => { // Use `chai-as-promised` for Promise like tests
    const bMock = {
      doOther: (num) => {
        expect(num).to.equal(123);
        return Promise.resolve('__PROXYQUIRE_HEY__')
      }
    };
    const A = proxyquire('./a', { './b': bMock });

    const instance = new A();
    const output = await instance.testThis(123);
    expect(output).to.equal('res for 123 is __PROXYQUIRE_HEY__');
  });
});
Run Code Online (Sandbox Code Playgroud)

使用 proxyquire 您可以轻松模拟依赖项的依赖项并对模拟的库进行期望。sinon用于直接监视/存根对象,通常必须同时使用它们。


sri*_*ger 5

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...
                });
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

但是,这并不是我所建议的。

我通常会尽量避免教条式的建议,但这将是少数例外之一。如果要进行单元测试,TDD或BDD,则通常应避免单例。它们与这些实践不能很好地融合在一起,因为它们使测试后的清理更加困难。在上面的示例中,这很简单,但是随着B类中添加了越来越多的功能,清理工作变得越来越繁重并且容易出错。

那你该怎么办呢?让您的B模块导出B类。如果要保留自己的DI模式并避免在B模块中使用该A模块,则B每次创建一个实例时只需创建一个新A实例。

遵循此建议,您的测试可能如下所示:

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...
                });
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

您会注意到,由于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...
                });
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

最后,与问题没有直接关系的最后建议- Promise构造函数永远不要用于包装promise。这样做是多余和混乱的,并且违反了使异步代码更易于编写的promise的目的。

Promise.prototype.then方法附带了一些有用的行为内置的,所以你永远不应该执行此多余的包装。调用then总是返回一个promise(此后称为“链接的promise”),其状态将取决于处理程序:

  • 一个then返回非承诺值处理将导致链的承诺,以解决该值。
  • then引发的处理程序将导致链式承诺拒绝使用引发的值。
  • 一个then返回一个承诺处理将导致链的承诺,以匹配返回承诺的状态。因此,如果它用一个值解决或拒绝,则链接的承诺将以相同的值解决或拒绝。

因此,您的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;
Run Code Online (Sandbox Code Playgroud)