nockでHTTP Request (axis)をモックしてjestでテストみた
Webアプリケーションのフロントエンドのテストを書く場合など、APIへのHTTPリクエストをモックしてテストしたい時 nock を使うとスッキリ書けて便利です。nock
を読み込むと http.ClientRequest
をオーバーライドしてくれて、リクエストに割り込み、特定のリクエストの場合モックしてくれるようになります。(ドキュメント)
axis
を使ってHTTPリクエストを送るサンプルコードを紹介してみたいと思います。
- サンプルコード
- プロジェクトのセットアップ
- nockで出てくる概念
- スコープ(scope)
- インターセプター(intercepter)
- 他のリクエストの情報(ヘッダやクエリパラメータ、リクエストボディ、ディレイ)指定の仕方
サンプルコード
プロジェクトを0から作ってテストを書いて理解を深められるように書いていますが、Githubにサンプルのテストコードを上げているのでgit cloneして動かしてみても良いかもしれません。 github.com
git cloneしてテストを実行できます。(2つ目のテスト(index_sample2.test.js)はFAILします)
$ git clone git@github.com:moritamori/nock-sample.git
$ npm i
$ npm run test
プロジェクトのセットアップ
プロジェクトを作り、必要なライブラリをインストールします。
$ mkdir nock-sample && cd nock-sample && npm init -y $ npm install nock axios jest --save-dev
ディレクトリ直下に index.js
を追加します。
// index.js const axios = require('axios') module.exports.getData = function() { return axios.get("https://qiita.com/api/v2/items") }
package.json
にtestをjestを実行するように変えておきます。
{ 〜〜〜 "scripts": { "test": "jest" }, 〜〜〜 }
nockで出てくる概念
まず nock
には、スコープ(scope)とインターセプター(intercepter)という2種類の概念があるのですが、モックするリクエストの情報を設定するためにどちらのオブジェクトの関数を呼ぶかを意識すると、うまくテストコードを書けると感じています。
nock v13の実装ベースで解説していこうと思います。
nock
は1つのスコープオブジェクトとインターセプターのリストを作るコードを書くことで、テスト実行時にインターセプターのリストにマッチするリクエストがあると、実際にHTTP リクエストを送る代わりにモックオブジェクトを返してくれます。
例えば、以下のようなスコープ・インターセプターがある場合では、
- インターセプター1
- 2回まで
https://qiita.com/api/v2/items
へのHTTPリクエストをモックし、レスポンス{ items: [] }
をステータス200で返す
- 2回まで
- インターセプター2
- 3回まで
https://qiita.com/api/v2/items/1
へのHTTPリクエストをモックし、レスポンス{ id: 1 }
をステータス200で返す
- 3回まで
という動きをします。
スコープ(scope)
スコープとは、ベースパス(プロトコル+ホスト名+ポート)やヘッダー情報といったリクエストの共通となる情報を持つオブジェクトです。
以下のように書くと、スコープオブジェクトが作られます。(参照した実装箇所)
// index.test.js const scope = nock("https://qiita.com")
インターセプター(intercepter)
インターセプターをスコープに追加
スコープオブジェクトのGet, Post関数等を呼ぶことで、インターセプターのオブジェクトが作られます。また、インターセプターの reply
関数を呼ぶことで、スコープオブジェクトのインターセプターのリストに追加され、テスト実行時にリクエストにマッチするインターセプターを探します。
プロジェクトのディレクトリ直下にindex.test.js
を追加して、テスト実行してみます。
/** * @jest-environment node */ const nock = require('nock') const index = require('./index') test('Quiita APIをモックしitems: []が返ってくること', async () => { // スコープオブジェクトを作成 const scope = nock('https://qiita.com') // パス`api/v2/items`にマッチするリクエストがあれば、代わりに `item: []` を返す const intercepter = scope.get('/api/v2/items') intercepter.reply(200, { items: [] }) // `/api/v2/items`にアクセスしてデータを取得 const res = await index.getData() // `item: []` が返ってくることを確認 expect(res.data).toEqual({ items: [] }) });
$ npm run test PASS ./index.test.js ✓ Quiita APIをモックしitems: []が返ってくること (21 ms)
モックするとインターセプターが削除されることを確認
インターセプターによって該当のHTTPリクエストがモックされると、スコープオブジェクトのリストから削除されます。(参照した実装箇所)
なので、テストコードを以下のように変えて、同じリクエストを送ると2回目のリクエストはモックされず、テストはFAILします。
/** * @jest-environment node */ const nock = require('nock') const index = require('./index') test('Quiita APIの2回目のリクエストがエラーになること', async () => { // スコープオブジェクトを作成 const scope = nock('https://qiita.com') // パス`api/v2/items`にマッチするリクエストがあれば、代わりに `item: []` を返す const intercepter = scope.get('/api/v2/items') intercepter.reply(200, { items: [] }) // `item: []` が返ってくることを確認 const res = await index.getData() expect(res.data).toEqual({ items: [] }) // 2回目はインターセプターが動かないため、items: []が返らない const res2 = await index.getData() expect(res2.data).toEqual({ items: [] }) });
$ npm run test FAIL ./index.test.js ✕ Quiita APIをモック (22 ms) ● Quiita APIをモック Nock: No match for request { "method": "GET", "url": "https://qiita.com/api/v2/items", "headers": { "accept": "application/json, text/plain, */*", "user-agent": "axios/0.19.2" } }
同じAPIに複数回リクエストを送るテストを実行する
同じAPIに複数回リクエストを送れるようにするには、以下のテストコードのように
- スコープのpersist関数を呼ぶ(何回でもインターセプターが動くように)
- インターセプターにモック回数(once, twice, thrice, times)(指定した回数インターセプターが動くように)
のどちらかを指定します。 両方指定した場合は、スコープの persist
が優先され、何度でもインターセプターが動くようになります。(参照した実装箇所)
/** * @jest-environment node */ const nock = require('nock') const index = require('./index') test('Quiita APIをモックしitems: []が返ってくること', async () => { // スコープオブジェクトを作成 const scope = nock('https://qiita.com') .persist() // 何度でもインタセープタが呼ばれるように // パス`api/v2/items`にマッチするリクエストがあれば、代わりに `item: []` を返す const intercepter = scope.get('/api/v2/items') .twice() // 2回までインタセプターを呼べるように (戻り値は、インターセプター) .thrice() // 3回までインタセプターを呼べるように (戻り値は、インターセプター) .times(5) // 5回までインタセプターを呼べるように (戻り値は、インターセプター) intercepter.reply(200, { 'items': [] }); // `item: []` が返ってくることを確認 const res = await index.getData(); expect(res.data).toEqual({ 'items': [] }); // 2回目もインターセプターが動くようになる const res2 = await index.getData(); expect(res2.data).toEqual({ 'items': [] }); });
また、インターセプターonce, twice, thrice
は内部的に times
関数を呼んでます。(参照した実装箇所)
インターセプターが動く回数毎に返すオブジェクトを変えたり、他のAPIをモックするには
インターセプターのreply
関数を呼ぶと、nock()で最初に作ったスコープオブジェクトが返ります。
なので、続けてGet, Post関数を呼ぶことでチェインして書けます。
/** * @jest-environment node */ const nock = require('nock') const index = require('./index') test('Quiita APIをモックすること', async () => { nock('https://qiita.com') .get('/api/v2/items') .once() // 1回目の`/api/v2/items`の呼び出しのみ `items: []`を返すように .reply(200, { 'items': [] }) // ここで、スコープオブジェクトが返る .get('/api/v2/items') .twice() //2,3回目の`/api/v2/items`の呼び出しのみ `items: [ { name: 'アイテム1' } ]`を返すように .reply(200, { 'items': [ { name: 'アイテム1' } ] }) // ここで、スコープオブジェクトが返る .get('/api/v2/items/1') //`/api/v2/items/1`の呼び出しは `{ id: 1 }`を返すように .reply(200, { id: 1 }) // `items: []` が返ってくることを確認 const res = await index.getData(); expect(res.data).toEqual({ 'items': [] }); // 2回目は `items: [ { name: 'アイテム1' } ]`が返ってくることを確認 const res2 = await index.getData(); expect(res2.data).toEqual({ 'items': [ { name: 'アイテム1' } ] }) // 3回目は `items: [ { name: 'アイテム1' } ]`が返ってくることを確認 const res3 = await index.getData(); expect(res3.data).toEqual({ 'items': [ { name: 'アイテム1' } ] }) });
他のリクエストの情報(ヘッダやクエリパラメータ、リクエストボディ、ディレイ)指定の仕方
ヘッダやクエリパラメータ、リクエストボディ、ディレイの指定に関しては、nockのREADMEを参考にしてみてください。
上記の記載したスコープ・インターセプターどちらのオブジェクトが返ってくるかを意識することで、複雑なテストケースに関してもうまく対応できるようになるかと思います。
実際のコードで、スコープ・インターセプターどちらにある関数なのか調べるのが確実です。