簡體   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