简体   繁体   English

如何使用sinon正确模拟ES6类

[英]How to properly mock ES6 classes with sinon

I want to be able to properly test my ES6 class, it's constructor requires another class and all this looks like this: 我希望能够正确测试我的ES6类,它的构造函数需要另一个类,所有这些看起来像这样:

Class A 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;

Class B B级

class B {
  constructor() {}

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

index 指数

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

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

Since I'm trying to do dependency injection rather than having requires at the top of the classes, I'm not sure how to go about mocking class B and it's functions for testing class A. 因为我正在尝试进行依赖注入,而不是在类的顶部需要,我不知道如何去模拟类B和它的测试类A的函数。

Sinon allows you to easily stub individual instance methods of objects. Sinon允许您轻松存根对象的各个实例方法。 Of course, since b is a singleton, you'll need to roll this back after every test, along with any other changes you might make to b . 当然,由于b是单身,因此您需要在每次测试后将其回滚,以及您可能对b进行的任何其他更改。 If you don't, call counts and other state will leak from one test into another. 如果不这样做,呼叫计数和其他状态将从一个测试泄漏到另一个测试。 If this kind of global state is handled poorly, your suite can become a hellish tangle of tests depending on other tests. 如果处理这种全局状态不佳,那么根据其他测试,您的套件可能会成为一种地狱般的混乱测试。

Reorder some tests? 重新排序一些测试? Something fails that didn't before. 事先没有失败。 Add, change or delete a test? 添加,更改或删除测试? A bunch of other tests now fail. 一堆其他测试现在失败了。 Try to run a single test or subset of tests? 尝试运行单个测试或测试子集? They might fail now. 他们现在可能会失败。 Or worse, they pass in isolation when you write or edit them, but fail when the whole suite runs. 或者更糟糕的是,它们在您编写或编辑时会孤立地传递,但在整个套件运行时会失败。

Trust me, it sucks. 相信我,它很糟糕。

So, following this advice, your tests can look something like the following: 因此,遵循此建议,您的测试可能如下所示:

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

However, this isn't quite what I would recommend. 但是,这不是我推荐的。

I usually try to avoid dogmatic advice, but this would be one of the few exceptions. 我通常会尽量避免教条式的建议,但这只是为数不多的例外之一。 If you're doing unit testing, TDD, or BDD, you should generally avoid singletons. 如果你正在进行单元测试,TDD或BDD,你通常应该避免单身。 They do not mix well with these practices because they make cleanup after tests much more difficult. 它们与这些实践并不完美,因为它们在测试后进行清理要困难得多。 It's pretty trivial in the example above, but as the B class has more and more functionality added to it, the cleanup becomes more and more burdensome and prone to mistakes. 在上面的示例中,它非常简单,但随着B类添加了越来越多的功能,清理变得越来越繁琐,容易出错。

So what do you do instead? 那你做什么呢? Have your B module export the B class. B模块导出B类。 If you want to keep your DI pattern and avoid requiring the B module in the A module, you'll just need to create a new B instance every time you make an A instance. 如果你想保持你的DI模式,避免要求B的模块A模块,你只需要创建一个新的B每次你做一个时刻A实例。

Following this advice, your tests could look something like this: 遵循此建议,您的测试可能如下所示:

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

You'll note that, because the B instance is recreated every time, there's no longer any need to restore the stubbed doSomething method. 您会注意到,因为每次都重新创建B实例,所以不再需要恢复存根的doSomething方法。

Sinon also has a neat utility function called createStubInstance which allows you to avoid invoking the B constructor completely during your tests. Sinon还有一个名为createStubInstance的简洁实用程序函数,它允许您在测试期间完全避免调用B构造函数。 It basically just creates an empty object with stubs in place for any prototype methods: 它基本上只是为任何原型方法创建一个带有存根的空对象:

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

Finally, one last bit of advice that's not directly related to the question-- the Promise constructor should never be used to wrap promises. 最后,最后一点与问题没有直接关系的建议 - Promise构造函数永远不应该用于包装promise。 Doing so is redundant and confusing, and defeats the purpose of promises which is to make async code easier to write. 这样做是多余和令人困惑的,并且违背了承诺的目的,即使异步代码更容易编写。

The Promise.prototype.then method comes with helpful behavior built-in so you should never have to perform this redundant wrapping. Promise.prototype.then方法带有内置的有用行为,因此您永远不必执行此冗余包装。 Invoking then always returns a promise (which I will hereafter call the 'chained promise') whose state will depend on the handlers: then调用总是返回一个promise(我将在下文中称之为'chained promise'),其状态将取决于处理程序:

  • A then handler which returns a non-promise value will cause the chained promise to resolve with that value. then ,返回非promise值的处理程序将导致链式承诺使用该值解析。
  • A then handler which throws will cause the chained promise to reject with the thrown value. then抛出的处理程序将导致链式承诺拒绝抛出的值。
  • A then handler which returns a promise will cause the chained promise to match the state of that returned promise. then返回promise的处理程序将使链接的promise与该返回的promise的状态相匹配。 So if it resolves or rejects with a value, the chained promise will resolve or reject with the same value. 因此,如果它以某个值结算或拒绝,则链式承诺将以相同的值解析或拒绝。

So your A class can be greatly simplified like so: 所以你的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;

I think you're searching for the proxyquire library. 我想你正在寻找代理图书馆。

To demonstrate this, I edited a little bit your files to directly include b in a (I did this because of your singleton new B ) , but you can keep your code, it's just more easy to understand proxyquire with this. 为了证明这一点,我编辑了一些你的文件直接包含b in (我这样做因为你的单身new B ,但是你可以保留你的代码,这更容易理解proxyquire。

b.js b.js

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

module.exports = new B();

a.js a.js

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

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

module.exports = A;

I want now to test a.js by mocking the behavior of b. 我现在想要通过a.js b的行为来测试a.js Here you can do this: 在这里你可以这样做:

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

Using proxyquire you can easily mock a dependency's dependency and do expectations on the mocked lib. 使用proxyquire,您可以轻松地模拟依赖项的依赖项,并对模拟的lib执行期望。 sinon is used to directly spy / stub an object, you have to use generally both of them. sinon用于直接间谍/存根对象,你必须经常使用它们。

Seems pretty straightforward, since sinon mocks an object by replacing one of its methods with a behavior (as described here ): 似乎相当简单,因为sinon通过用行为代替它的方法(如所描述的嘲笑对象这里 ):

(I added resolve() -s to both of the promises in your functions to be able to test) (我为你的函数中的两个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(); }); }); }); 

Please let me know if I misunderstood something or additional detail what you'd like to test... 如果我误解了你要测试的东西或其他细节,请告诉我...

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

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