ちょっとした仕事で、ちゃんとした、 JavaScript のテストコードを書く必要性が生じた。動くテストはかけるが、あまり慣れていない言語で「適切な」テストコードがかけているかは正直わからない。まとまったラーニングコースを受けてみるつもりだが、2ビジネス営業日かかるみたいなので、まず最初に作ってみた。
テストフレームワークをどれを使うかはあたりがついている。Mochaとchai。これは、TypeScriptの頃から同じなので、馴染みがある。そもそもBDD形式のフレームワークなので、使い方はどれも似ている。
Mocha は BDDのテストフレームワークで、chai は, expect
などのアサーション部分をBDDフレームワークの形式で書けるようになっている。インストールは次の通り
npm init -y
npm install mocha chai --save-dev
ざっくりと仕様を確認する。
Mocha
下記のサンプルをみるとそれだけで雰囲気がわかるだろう。他のBDDフレームワークと違いはない。node なのでコールバックの形式になっている。
var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal(-1, [1,2,3].indexOf(4));
});
});
});
メソッドは、Promise でも書けるし
const assert = require('assert');
it('should complete this test', function (done) {
return new Promise(function (resolve) {
assert.ok(true);
resolve();
})
.then(done);
});
async/await でも書ける様子。
beforeEach(async function() {
await db.clear();
await db.save([tobi, loki, jane]);
});
describe('#find()', function() {
it('responds with matching records', async function() {
const users = await db.find({ type: 'User' });
users.should.have.length(3);
});
});
残りは、おなじみの Setup/Teardown だがこんな感じ。
describe ("hooks", function() {
before(function(){
console.log("before called");
});
after(function(){
console.log("after called");
});
beforeEach(function() {
console.log("beforeEach called");
});
afterEach(function(){
console.log("afterEach called");
})
it ("should be true", (done) => {
console.log("test 01 called");
done();
});
it ("should be false", (done) => {
console.log("test 02 called");
done();
})
})
実行結果
$ npm test
> mock@1.0.0 test /Users/ushio/Codes/node/mock
> mocha **.spec.js
hooks
before called
beforeEach called
test 01 called
✓ should be true
afterEach called
beforeEach called
test 02 called
✓ should be false
afterEach called
after called
2 passing (6ms)
簡単ですな。
Chai
アサーションの部分。Expect
で当然書きたいよね。だから、これで。文法は見ての通り。
var expect = require('chai').expect
, foo = 'bar'
, beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);
Sinon
コードを書いていて、今回はスタブを書く必要があった。ちなみに、モックとスタブの違いは次の記事が良かった。
スタブは受信メッセージのために使い、モックは送信メッセージのために使います。メソッドそのものをテストしたくて、偽のオブジェクトを渡したい時は、スタブ、メソッドの中で使われているオブジェクトに渡ってくる値、呼ばれた回数などを検査したい時はモックという名前で呼ばれます。どちらも偽オブジェクトですが用途が違います。これに加えて、Spy という機能も持ったフレームワークが
です。シノーンはギリシャ神話に出てくる人で、トロイの木馬のそばにいて、なんのためにその木馬が作られたなどについて正しいことを言わなかった。それによって、トロイの木馬が城内に持ち込まれた。というエピソードで出てくる人みたい。
Sinon.js では、Spy, Stub, Mock が使える。
によると
名称 | 意味 |
---|---|
Stubbing | スタブしたファンクションは、呼ばれず、代わりにあなたが提供した実装が呼ばれる。もし、あなたが用意してなかった、空の関数が呼ばれる |
Spying | スパイしたファンクションは、元の実装が呼ばれる。ただ、あたはそれのアサーションを実施できる |
Mocking | スタブと同じだが、ファンクションだけではなく、オブジェクトに使える |
と定義がいささか違う。今回は、ある関数をテストするのに、渡ってくる引数を偽物にして、関数をテストしたかったので、stub
が良さげだが、結局Spyにしている。
月から実際のコードをみていこう。
テスト対象コード
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
if (req.query.name || (req.body && req.body.name)) {
context.res = {
// status: 200, /* Defaults to 200 */
body: "Hello " + (req.query.name || req.body.name)
};
context.bindings.outputQueueItem = req.query.name;
}
else {
context.res = {
status: 400,
body: "Please pass a name on the query string or in the request body"
};
}
context.done();
};
このパラメータのオブジェクトを騙したい。テストを書いてみた。
npm install sinon --save-dev
'use strict'
const Scheduler = require('../Scheduler/index.js')
const expect = require('chai').expect
const sinon = require('sinon');
describe('Scheduler function', () => {
var req = {};
var context = {};
beforeEach(function() { // 1. Stub
req = {
query: {
name: "Azure"
},
body: {
name: "taro"
}
};
context = {
res: {
status: 200
},
log: function (str) {
console.log(str);
},
bindings: {
outputQueueItem: "fake_queue_value"
},
done: function () {
}
};
});
it('should pass query name to queue bindings', (done) => {
sinon.spy(context, "done"); // 2. Spy
Scheduler(context, req);
expect(context.bindings.outputQueueItem).to.equal("Azure", "Queue isn't set");
expect(context.done.called).to.be.true
context.done.restore();
done();
})
it ('should be ok if the query name has been passed', (done) => {
Scheduler(context, req);
expect(context.res.body).to.equal("Hello Azure");
done();
})
it ('should not be ok if the query name isnt supplied', (done) => {
req = {
query: {
},
body: {
}
};
Scheduler(context, req);
expect(context.res.status).to.equal(400);
expect(context.res.body).to.equal("Please pass a name on the query string or in the request body");
done();
})
})
確かにこれでテストができる。期待通りに動作する。
$ npm test
> tests@1.0.0 test /Users/ushio/Codes/AzureFunctions/nodeci/node/Tests
> mocha *.spec.js
Scheduler function
JavaScript HTTP trigger function processed a request.
✓ should pass query name to queue bindings
JavaScript HTTP trigger function processed a request.
✓ should be ok if the query name has been passed
JavaScript HTTP trigger function processed a request.
✓ should not be ok if the query name isnt supplied
3 passing (9ms)
{
"name": "tests",
"version": "1.0.0",
"description": "UnitTest project",
"main": "index.js",
"scripts": {
"test": "mocha *.spec.js",
"report": "mocha *.spec.js --reporter mocha-junit-reporter"
},
"author": "",
"license": "ISC",
"devDependencies": {
"chai": "^4.1.2",
"mocha": "^4.0.1",
"mocha-junit-reporter": "^1.15.0",
"sinon": "^4.1.2"
}
}
ポイントは、1. Stub
で偽オブジェクトを作っている。2. Spy
で done()
メソッドをスパイして、実際に呼ばれたかを context.done.called
メソッドで確認している。このスパイのオブジェクトは、context.done.restore()
で元のオブジェクトに戻される。
stub
を使おうと思ったが、結局元のオブジェクトが無いといけなかったり、オブジェクトの値を変更できない感じだったので、今のコードになっている。もっと良いベストプラクティスは無いのだろうか。 次回はもっと適切な書き方を探ってみたい。
こうしたらもっといいよー。というのがあれば是非コメントをお願いいたします。