【问题标题】:How to properly mock ES6 classes with sinon如何使用 sinon 正确模拟 ES6 类
【发布时间】:2018-10-27 15:15:11
【问题描述】:

我希望能够正确测试我的 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;

B 类

class B {
  constructor() {}

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

索引

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

const a = new A(b);
a.doSomething(123)
  .then(() => {
    // things
  });

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

【问题讨论】:

  • 很高兴看到您如何在测试代码中使用这些模块。我不太明白你的 index 模块的目的。
  • index 是主要的应用程序......这只是一个过于简单的例子。
  • 好的。所以当你测试 A 类时,你会得到类似 require('./a') 的东西,而根本不接触 index,那么?
  • 是的,所以单元测试A...我想我需要为索引创建一个集成测试?
  • 你可以,如果你觉得你需要它。名为index.js 的文件通常用作目录索引,有时人们会通过这些文件将模块导入单元测试。我只是想确保这里不是这种情况。稍后我会有一个完整的答案。

标签: javascript node.js mocha.js bdd sinon


【解决方案1】:

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;

【讨论】:

    【解决方案2】:

    看起来很简单,因为sinon 通过用行为替换其方法之一来模拟对象(如here 所述):

    (我将resolve()-s 添加到您的函数中的两个promise 中以便能够测试)

    const sinon = require('sinon');
    
    const A = require('./A');
    const b = require('./b');
    
    describe('Test A using B', () => {
      it('should verify B.doOther', async () => {
        const mockB = sinon.mock(b);
        mockB.expects("doOther").once().returns(Promise.resolve());
    
        const a = new A(b);
        return a.doSomething(123)
          .then(() => {
            // things
            mockB.verify();
          });
      });
    });

    如果我对您想要测试的内容或其他细节有误解,请告诉我...

    【讨论】:

      【解决方案3】:

      我认为您正在搜索 proxyquire 库。

      为了证明这一点,我编辑了一点你的文件以将 b 直接包含在 a 中(我这样做是因为你的单例 new B),但你可以保留你的代码,它只是更多用这个很容易理解代理查询。

      b.js

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

      a.js

      const b = require('./b');
      
      class A {
        testThis(number) {
          return b.doOther(number)
            .then(result => `res for ${number} is ${result}`);
        }
      }
      
      module.exports = A;
      

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

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

      使用 proxyquire,您可以轻松地模拟依赖项的依赖项并对模拟的库进行预期。 sinon 用于直接 spy / stub 一个对象,你通常必须同时使用它们。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-10-24
        • 1970-01-01
        • 2022-01-05
        • 2017-12-18
        • 1970-01-01
        • 2017-09-30
        • 2018-08-28
        • 1970-01-01
        相关资源
        最近更新 更多