node.jsのソースを単体テストする際のmocha+sinonの使い方
node.jsのソースをmochaでテストしているとsinonでスタブしてテストする、といったケースがあると思います。
sinonは英語ドキュメントもありますし、広く使われているモジュールなのでスタブの方法がネットにごろごろ転がっています。
sinonでメソッドをスタブする
簡単なソースでsinonを試してみたいと思います。
以下をindex.jsで保存します。
function testMethod(money=0){ money = money * 1.08; return money; }; exports.sample = testMethod;
exports.sampleという変数にtestMethodを代入します。これでtestMethodをsampleという名前で他のモジュールからも使えるようにします。
このtestMethodの戻り値をsinonでスタブしてみます。
以下、index.test.jsです。
const chai = require('chai'); const sinon = require('sinon'); const assert = chai.assert; const expect = chai.expect; const target = require('../../main/sample001/index.js');// requireする describe('sinon', function () { it('sinonのテスト', function(){ let stub = sinon.stub(target,'sample');// targetのsampleをスタブする let expected = 110; stub.returns(expected);// sampleの戻り値は110にしてみる let ret = target.sample(50); console.log(ret); expect(expected).to.equal(ret); }); });
stub.returnsで通常の戻り値(Promiseなどではない)を指定しているので、これでスタブができたことになります。
そのあとにsample()を実行すると引数が何であっても戻り値は指定した戻り値となります。
結果は以下のようになります。
sinonでメソッド内のメソッドをスタブする
実際はメソッド内にあるDBに検索するメソッドなどをスタブするというケースが多いかと思います。
以下はメソッド内のメソッドをスタブする例です。
function testMethod(money=0){ let self = this; money = money * self.getTax();// 消費税を別のメソッドから取得 return money; }; function getTax() { return 1.08;// DBから取得しているイメージ } module.exports = { sample: testMethod, getTax: getTax };
getTaxは通常はDBから取得すると思いますが、スタブできるかのテストなので1.08を返すだけにしました。
index.test.jsで1.08を返すのをスタブ化し、1.10を返すようにしています。テスト自体はtestMethodメソッドから実行しています。
const chai = require('chai'); const sinon = require('sinon'); const assert = chai.assert; const expect = chai.expect; const target = require('../../main/sample001/index.js'); describe('sinon', function () { it('sinonのテスト', function(){ let stub = sinon.stub(target,'getTax');// スタブする let val = 1.10; stub.returns(val);// getTaxメソッドの戻り値は1.10 let ret = target.sample(100);// インプットは100 let expected = 110;// 期待値は110 expect(expected).to.equal(parseInt(ret)); }); });
sandboxでオブジェクトのキーに対するプロパティをスタブする
sinon.sandbox.create()メソッドでsandboxオブジェクトを返します。
※sandbox.create()は非推奨となっています
変わりにrequire('sinon').createSandbox()
メソッドを使用します。
sandbox.stub(オブジェクト,'キー').values('スタブしたい値')
このように記述するとオブジェクトのキーに対するプロパティをスタブすることができます。
以下、実行例です。
const chai = require('chai') const sandbox = require('sinon').createSandbox() const assert = chai.assert const expect = chai.expect describe('sinon', function () { it('sinonのテスト', function(){ const myObject = { 'hello': 'world' } sandbox.stub(myObject, 'hello').value('Sinon'); // valueの代わりにreturnsも可 let expected = 'Sinon'; expect(expected).to.equal(myObject.hello); }) afterEach(() => { sandbox.restore() }) after(() => { }) });
結果は成功します。
AWS-SDKをスタブする
AWS.Service.prototypeのmakeRequestをスタブします。返すのはrejectするかresolveするかです。
以下はSNSのconfirmSubscriptionメソッドをスタブした例です。
テストコード
const aws = require('aws-sdk') ~~ ~~ const stub = sandbox.stub(AWS.Service.prototype, 'makeRequest') stub.returns({ promise: () => { return Promise.resolve({'SubscriptionArn':'fuga'}) } })
ソース
const aws = require('aws-sdk') const sns = new aws.SNS({ apiVersion: '2010-03-31', region: 'ap-northeast-1' }) ~~ ~~ const ret = await sns .confirmSubscription({ TopicArn: '', Token: '' }).promise() // モックできる
Promsie.allの戻り値をresoleveまたはrejectします。
sandbox.stub(Promise. 'all').resolves() sandbox.stub(Promise, 'all').rejects()
自作のUtilityモジュールのgetObjectメソッド(static async function)をスタブする例です。
sandbox.stub(Utility, 'getObject').resolves({ "Body": '{ "intercomDeviceId": "00c08f445566" }' })
sinonのエラー
Cannot stub non-existent own property XXX
…XXXというメソッドがない場合にこのエラーが発生する。
TypeError: Attempted to wrap execute which is already wrapped
…すでにスタブしているものをもう一度スタブしようとすると発生する。
sinonの評価はmocha+chaiの使い方を参照して下さい。
process.env(環境変数)をスタブする方法
process.env(環境変数)をスタブするにはsandboxを使用します。
createSandboxメソッドでsandboxを作成し、afterEachでサンドボックスのrestoreします。
stubsとsandboxの最新ドキュメントです。
const sandbox = require('sinon').createSandbox() //sandbox作成 afterEach(()=>{ sandbox.restore()// sandboxをリストアすること }) after(()=>{ })
環境変数をスタブします。
sandbox.stub(process.env, 'PATH').value('12345');// 環境変数PATHを12345にスタブ
上記で環境変数PATHの値を変えることができます。但し、環境変数がPCに設定されていない場合は以下のようなエラーとなります。
TypeError: Cannot stub non-existent own property PATH
以下、実行例です。
const chai = require('chai') const sandbox = require('sinon').createSandbox() const expect = chai.expect describe('sinon', function () { it('sinonのテスト', function(){ sandbox.stub(process.env, 'PATH').value('12345') expect(process.env.PATH).to.equal('12345') // passになる }) afterEach(function () { sandbox.restore() // sandboxをリストアすること }) after(function () { }) })
オブジェクトをスタブする
sinon.stubメソッドは第一引数でオブジェクトだけを指定することができます。
sinon.stub(target);
この場合、targetが持つすべてのメソッドをスタブすることになります。
targetはaaa,bbb,cccと言うメソッドを持つとします。その場合、以下のように全てのメソッドをrestore()する必要があります。
beforeEach(function (done) { stub = sinon.stub(target);// 第二引数でメソッドは指定しない done(); }); afterEach(function (done) { stub.aaa.restore(); stub.bbb.restore(); stub.ccc.restore(); }
メソッドの戻り値を動的に変更する
メソッドをスタブすることができますが、1回目の戻り値と2回目の戻り値を変更したい場合があります。
以下のようにonCallメソッドを使用します。
const stub = sinon.stub(target.service, 'getDb'); stub.onCall(0).returns(Promise.resolve('1'));// 1回目は1を返す stub.onCall(1).returns(Promise.resolve('2'));// 2回目は2を返す
onCall(0)が1回目の戻り値になります。
onCall(1)が2回目の戻り値になります。
sandbox.stub().returnsとresolvesとyieldsの違い
returns
は通常の戻り値を指定します。
resolves
とrejects
はPromiseを指定します。
コールバック関数を持つ関数には yields
またはcallArgsWith
を使用します。
sinonのアサーション
sinonには標準でアサーションが用意されています。
sinon.assert.called(index.tax) // 呼ばれたことの検証 sinon.assert.calledOnce(index.tax) // 1回呼ばれたことの検証 sinon.assert.notCalled(index.tax) // 呼ばれていないことの検証 sinon.assert.calledTwice(index.tax) // 2回呼ばれたことの検証 sinon.assert.callOrder(index.tax, index.age) // スタブしたメソッドの呼ばれた順序の検証 sinon.assert.callCount(index.tax, 3) // スタブしたメソッドの呼ばれた回数の検証
sandboxのアサーションです。Promise.allが何回呼ばれたかを検証しています。
sandbox.stub(Promise, 'all').resolves() // 処理 sandbox.assert.calledOnce(Promise.all)
calledOnce,calledTwice,calledThriceなどが用意されています。
戻り値メモ
listObjectsV2メソッドの戻り値例
const listObjectsV2Ret = { IsTruncated: false, Contents: [ { Key: 'var/tmp/a.json', last_modified: '2020-07-07T09:40:57.000Z', etag: '"14758w1afd44c09b7456167wwa00b43d"', size: 24, storage_class: 'STANDARD' } ], Name: 'bucket', Prefix: 'var/tmp/', MaxKeys: 1000, KeyCount: 1 }
newしているクラスをモックする
クラスをモックする方法です。
ソース
module.exports.Index = class Index { constructor() {} async _get() { // ~~ } }
テストコード
const Index = require('../src/index').Index const sandbox = require('sinon').createSandbox() describe('', async () => { it('',async () => { sandbox.stub(Index.prototype, '_get').callsFake(() => { return {} }) // ~~ }) })
KHI入社して退社。今はCONFRAGEで正社員です。関西で140-170/80~120万から受け付けております^^
得意技はJS(ES6),Java,AWSの大体のリソースです
コメントはやさしくお願いいたします^^
座右の銘は、「狭き門より入れ」「願わくは、我に七難八苦を与えたまえ」です^^
コメント