繁体   English   中英

如何使用sinon正确模拟ES6类

[英]How to properly mock ES6 classes with 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;

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的函数。

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模式,避免要求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...
                });
        });
    });
});

您会注意到,因为每次都重新创建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 ,返回非promise值的处理程序将导致链式承诺使用该值解析。
  • then抛出的处理程序将导致链式承诺拒绝抛出的值。
  • 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;

我想你正在寻找代理图书馆。

为了证明这一点,我编辑了一些你的文件直接包含b in (我这样做因为你的单身new B ,但是你可以保留你的代码,这更容易理解proxyquire。

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;

我现在想要通过a.js 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,您可以轻松地模拟依赖项的依赖项,并对模拟的lib执行期望。 sinon用于直接间谍/存根对象,你必须经常使用它们。

似乎相当简单,因为sinon通过用行为代替它的方法(如所描述的嘲笑对象这里 ):

(我为你的函数中的两个promises添加了resolve() s以便能够测试)

 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(); }); }); }); 

如果我误解了你要测试的东西或其他细节,请告诉我...

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM