ミドルウェアの単体試験は何を試験するの?
Node.js + express で Web アプリケーションを構築する際、ミドルウェアの単体試験についてまとまった情報が見つからずハマりました。
そもそもミドルウェアで行われる処理は、「この条件の時はエラーなので res.status(400).render('./error_page')
して終了」というようなことが多いです。
したがって試験の網羅率を上げるには、これらのメソッドが呼ばれたことを検出する必要があります。
next()
ぐらいなら単純に sinon.spy()
で検出できますが、res.status().render()
のようなチェーンメソッドの場合には工夫が必要だったので、試行錯誤した結果をまとめます。
各種前提
ソフトウェアバージョン
- node.js v12.17.0
- express 4.17.1
- chai 4.2.0
- sinon 9.0.1
- mocha 8.8.1
本記事のスコープ
- npm モジュール
chai
,mocha
,sinon
を用いた express ミドルウェアの単体試験方法。
本記事のスコープではないもの
- node.js に関する基礎知識。
- express のミドルウェアに関すること。
- npm モジュール
chai
,mocha
,sinon
に関する基礎知識。 - Promise なミドルウェア。
ミドルウェアと単体試験コードの作成
ミドルウェアの作成
リクエストヘッダにパラメータ authorization
が存在するかどうかをチェックするミドルウェアを想定します。
'use strict';
const checkHeader = () => {
return (req, res, next) => {
if (!req.headers.authorization) {
return res.status(400).json({ message: 'invalid request' });
}
return next();
}
}
module.exports = checkHeader;
試験項目
- 正常系
-
next()
が1回呼ばれること。 -
res.status()
やres.json()
が呼ばれないこと。
-
- 異常系
-
res.status()
が1回呼ばれること。 -
res.status()
の引数が400
であること。 -
res.json()
が1回呼ばれること。 -
next()
が呼ばれないこと。
-
テストの作成
基本的な構造
まずは next()
が1回呼ばれることを確認するだけのテストを作成します:
/* eslint-env mocha */
'use strict';
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
const checkHeader = require('../../../../routes/middlewares/checkHeader.js');
describe('checkHeader の単体試験', () => {
describe('正常系', () => {
it('リクエストヘッダーに authorization が存在する', (done) => {
const req = {};
req.headers = { authorization: 'something' };
const res = {};
const next = sinon.spy();
// ミドルウェアの実行
checkHeader()(req, res, next);
// アサーション
assert(next.calledOnce);
done();
});
});
});
メソッドチェーンの対処
res.status().json()
のようなメソッドチェーンのテストには、sinon.spy()
を仕込んだクラスを使います:
class responseSpy {
constructor() {
this.statusSpy = sinon.spy();
this.jsonSpy = sinon.spy();
}
status(arg) {
this.statusSpy(arg);
return this;
}
json() {
this.jsonSpy();
return this;
}
}
このクラスのインスタンスを res
に渡せば、res.status()
や res.json()
のコール状況が確認できます:
assert(res.statusSpy.notCalled);// res.status() が呼ばれていないこと
assert(res.jsonSpy.notCalled);// res.json() が呼ばれていないこと
今回は responseSpy
クラスに実装したメソッドは status()
と json()
だけです。
必要に応じて send()
や render()
等を実装してください。
最終的なテストは次の通りです:
/* eslint-env mocha */
'use strict';
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
// テスト対象のミドルウェアを読み込む
const checkHeader = require('../../../../routes/middlewares/checkHeader.js');
// res に使うクラスを作成
class responseSpy {
constructor() {
this.statusSpy = sinon.spy();
this.jsonSpy = sinon.spy();
}
status(arg) {
this.statusSpy(arg);
return this;
}
json() {
this.jsonSpy();
return this;
}
}
describe('checkHeader の単体試験', () => {
describe('正常系', () => {
it('リクエストヘッダーに authorization が存在する', (done) => {
// ミドルウェアが使う req, res, next を定義
const req = {};
req.headers = { authorization: 'something' };
const res = new responseSpy();// 先に定義したクラスをインスタンス化
const next = sinon.spy();
// ミドルウェアの実行
checkHeader()(req, res, next);
// アサーション
assert(next.calledOnce);
assert(res.statusSpy.notCalled);// res.status() が呼ばれていないこと
assert(res.jsonSpy.notCalled);// res.json() が呼ばれていないこと
done();
});
});
describe('異常系', () => {
it('リクエストヘッダーに authorization が存在しない', (done) => {
const req = {};
req.headers = {};
const res = new responseSpy();
const next = sinon.spy();
checkHeader()(req, res, next);
assert(res.statusSpy.calledOnce);// res.status() が1回だけ呼ばれていること
assert(res.statusSpy.withArgs(400));// res.status() の引数が 400 であること
assert(res.jsonSpy.calledOnce);// res.json() が1回だけ呼ばれていること
assert(next.notCalled);
done();
});
});
});
テスト実行
テストを実行すると次のように成功するはずです。
$ npx mocha --exit ./test/unit/routes/middlewares/checkHeader.test.js
checkHeader の単体試験
正常系
✓ リクエストヘッダーに authorization が存在する
異常系
✓ リクエストヘッダーに authorization が存在しない
2 passing (22ms)
雑記
- ミドルウェアは単体ではなく結合で試験するのでは?
- いやいや単体でしょう。
- 単体試験で
next()
やres.status()
を引っ掛けるというのはアリなのだろうか?- ミドルウェアだと条件分岐に入ったら
next()
して終わり、ということが多いし・・・仕方ないのでは?
- ミドルウェアだと条件分岐に入ったら
- ミドルウェアで Promise 使う時、例えば DB 参照する時に
next()
しても、なぜか次のミドルウェアに進んでくれないことがある。return next()
すると期待した動作をする。あれ何なんだろう。 - とりあえずミドルウェアで
next()
,res.render()
等をするときは明示的にreturn
するようにしているが、これが果たして良いことなのか調査中。