概要
APIクライアントのユニットテストでは、CI環境で実行する場合などに通信先のサーバが用意しがたいため、通信先のサーバのmockを用意する方法が取られる。
一般的なmockでは、どのようなリクエストに対してどのようなレスポンスを返すかという情報を別途記述する方法があるが、用意するのが面倒であったり、実際のサーバのレスポンスと必ずしも一致しない可能性がある、というデメリットがある。
そこで、Rubyのvcrライブラリなどでは、リクエストに対するサーバのレスポンスをファイルに記録し、そのファイルがあれば次にテストを実行したときにmockとして動かすことができる、というアプローチを取っている。
Node.jsでも同じようなライブラリがないか探したところ、nockの Nock Back という機能が同様に利用できそうだったので、その用例を記述する。
使ってみる
http://httpbin.org/get にGETリクエストを投げる場合を考えると、Nock Backを使うと以下のようなコードが記述できる。
const axios = require('axios');
const nockBack = require('nock').back;
// 1. Setup Nock Back
nockBack.setMode('record');
nockBack.fixtures = __dirname + '/nockFixtures';
// 2. Test Function
const test = async () => {
const { nockDone } = await nockBack('httpbin.json');
const res = await axios.get('http://httpbin.org/get');
console.log(res.data);
nockDone();
};
test().then(() => {
process.exit(0);
}).catch((e) => {
console.log(e.message);
process.exit(1);
});
1. Setup Nock Back
では、Nock Back共通の設定を行なっている。
nockBack.setMode()
はNock Backの動作モードを指定している。ここで指定している 'record'
は、記録されたファイル(以下ではfixtureと呼ぶ)がなければ記録し、fixtureがあれば使うというモードである。これは NOCK_BACK_MODE
という環境変数でも指定できるので、環境によってモードを使い分けたい場合などはそちらで指定した方が良い。
nockBack.fixtures
はfixtureを保存するディレクトリを指定する。
2. Test Function
では、HTTPリクエストを送信する前後に nockBack()
と nockDone()
の呼び出しを行なっている。この間のHTTPリクエスト・レスポンスが全てfixtureに記録される。
上記のスクリプトを動かすと、以下のような挙動となることが確認できる。
- 1回目の呼び出し: HTTPリクエストが送信され、fixture (
nockFixtures/httpbin.json
) が作成される - 2回目以降の呼び出し: HTTPリクエストが送信されない
可変値を含むリクエストの場合
タイムスタンプやランダム値などをリクエストボディに含むHTTPリクエストの場合、リクエストごとにボディの中身が変わってしまうため、1回目のリクエストで生成されたfixtureが2回目のリクエストではアンマッチとなり、エラーとなってしまう。
例えば、上記の 2. Test Function
でのAPI呼び出しを以下のように書き換えると、2回目のリクエストではエラーとなってしまう。
const res = await axios.post('http://httpbin.org/post', { timestamp: (new Date()).toString() });
このようなリクエストに対応するには、Nock Backのオプションにより、リクエストボディを書き換える設定を行うことが必要である。
具体的には、以下のようなコードを記述すれば良い。
const nockBackOptions = {
before: function (scope) {
scope.filteringRequestBody = function (body, aRecordedBody) {
const fixedBody = JSON.parse(body);
if (aRecordedBody.timestamp && fixedBody.timestamp) {
fixedBody.timestamp = aRecordedBody.timestamp;
return JSON.stringify(fixedBody);
}
return body;
}
},
};
const test = async () => {
const { nockDone, context } = await nockBack('httpbin.json', nockBackOptions);
const res = await axios.post('http://httpbin.org/post', { timestamp: (new Date()).toString() });
console.log(res.data);
nockDone();
};
特筆すべきは、 scope.filteringRequestBody
メソッドの定義である。
このメソッドでは、第一引数に実際のリクエストボディ、第二引数にfixture上のリクエストボディが渡され、返り値として実際のリクエストボディを置き換えた値を渡すことができる。
上の処理では、リクエストボディにtimestampという値があれば、fixture上のtimestampの値をコピーした値を返すことで、timestampの値の違いを無視させるようにしている。また、リクエストボディがJSONとなる場合、body
がString, aRecordedBody
はObjectとなっていたため、一旦Objectに変換して置き換えを行ない、Stringに戻している。
運用について
そのほか、開発時の運用について考えたことをメモする。
- 開発時はデフォルトで
NOCK_BACK_MODE=record
とし、CI環境ではNOCK_BACK_MODE=lockdown
(HTTPリクエストを発行せず、必ずfixtureファイルを使う) としておけば、fixtureファイルのアップロード漏れを防ぎ、CI環境をより安定にできる - fixtureを修正する必要がある場合(テスト対象のリクエストやレスポンスが変わった場合など)は、今のところ対象のfixtureを削除してテストを再実行するしかなさそう (fixtureを上書きするモード作成についてのIssueも出ているが、執筆時点では実装されていない) なので、READMEなどに書いておくかnpm scriptにファイル削除処理も組み込んだスクリプトを用意しておくのが良さそう
環境情報
- Node.js 8.11.2
- nock 9.4.2