fetch
は、JavaScript で HTTP 通信を行うための API。SPA で Web API とやり取りを行う際などによく使われる。
例えば以下のコンポーネントでは、ボタンを押下するとfetch
によるリクエストが発行され、そのレスポンスに応じて表示内容が変わる。
import React, {useState} from 'react'; export const Sample = () => { const [result, setResult] = useState('Please press button'); const callApi = (statusCode) => { setResult('wait...'); fetch(`https://httpbin.org/status/${statusCode}`).then((res) => { const text = res.status >= 200 && res.status < 300 ? 'success' : 'failure'; setResult(text); }); }; return ( <> <div>{result}</div> <button type="button" onClick={() => callApi(200)}> call 200 </button>{' '} <button type="button" onClick={() => callApi(500)}> call 500 </button> </> ); };
このときに問題になるのが、テスト。
テスト環境にはfetch
が存在しないため、何も対策をしないとエラーになってしまう。
そして、このコンポーネントに対してテストしたいのは、ボタンを押下した際にリクエストを行い、レスポンスに応じて表示内容を書き換えるか、である。そのため、実際にリクエストを行う必要はなく、fetch
のモックさえあれば十分である。
fetch-mock
を使うことで、簡単にfetch
のモックを作ることができる。
この記事では、先程のコンポーネントのテストを書きながら、fetch-mock
の具体的な使い方を説明する。
以下のライブラリで、動作確認している。
- fetch-mock@9.5.0
- node-fetch@2.6.0
- react@16.13.1
- jest@26.0.1
- @testing-library/react@10.0.4
- power-assert@1.6.1
まず、fetch
が実行されることのないテスト、つまりボタンを押下しないテストを書いてみる。
import React from 'react'; import {render, screen} from '@testing-library/react'; import assert from 'assert'; import {Sample} from '../Sample'; describe('Sample', () => { it('Initially, "Please press button" is displayed', () => { render(<Sample />); assert(screen.queryByText('Please press button')); }); });
画面上にPlease press button
という文言があるか、というテストだが、これは問題なくパスする。
このテストに、ボタンを押下した際のテストを加えてみる。
import React from 'react'; import {render, fireEvent, screen} from '@testing-library/react'; import assert from 'assert'; import {Sample} from '../Sample'; describe('Sample', () => { beforeEach(() => { render(<Sample />); }); it('Initially, "Please press button" is displayed', () => { assert(screen.queryByText('Please press button')); }); describe('Press "call 200"', () => { it('"success" is displayed', async () => { fireEvent.click(screen.getByRole('button', {name: 'call 200'})); assert(screen.queryByText('wait...')); const res = await screen.findByText('success'); assert.strictEqual(res.textContent, 'success'); }); }); describe('Press "call 500"', () => { it('"failure" is displayed', async () => { fireEvent.click(screen.getByRole('button', {name: 'call 500'})); assert(screen.queryByText('wait...')); const res = await screen.findByText('failure'); assert.strictEqual(res.textContent, 'failure'); }); }); });
このテスト環境にはfetch
がないため、エラーになる。
ReferenceError: fetch is not defined
fetch-mock
を使って対策を行うが、その前に上記のテストコードで使っているfindByText
について補足する。
testing-library
が提供しているfindBy*
は、要素を取得するか、もしくはタイムアウトするまで、待機する API。
返り値はPromise
で、要素が取得できた場合は、その要素を値としてresolve
される。要素が見つからずにタイムアウトしたか、または複数の要素を取得した場合は、reject
される。
findAllBy*
も同様だが、resolve
の値が要素の配列になることと、複数の要素を取得してもreject
されないことが、異なる。
今回のように非同期で表示内容が変化する要素を取得したい場合は、get
やquery
ではなくfind
を使う必要がある。
まずwait...
に変化しているかをテストし、その後、レスポンスに応じた内容に変化するまで待機する。
この書き方に問題はなく、あとはfetch
のモックさえ用意すればパスするはずである。
まず、必要なライブラリをインストールする。
$ yarn add -D fetch-mock node-fetch
あとは、テストコードのなかでfetchMock
をimport
し、必要な設定を行うだけである。
@@ -1,10 +1,19 @@ import React from 'react'; import {render, fireEvent, screen} from '@testing-library/react'; +import fetchMock from 'fetch-mock'; import assert from 'assert'; import {Sample} from '../Sample'; describe('Sample', () => { + fetchMock + .get('https://httpbin.org/status/200', { + status: 200, + }) + .get('https://httpbin.org/status/500', { + status: 500, + }); + beforeEach(() => { render(<Sample />); });
これで、パスするようになる。
.get(url, {status: 200})
とすることで、url
にGET
リクエストを送った際に、ステータスコード200
のレスポンスを返すようになる。
上記のようにメソッドをつなげていくことが可能で、一度に複数の設定を行える。
GET
以外のメソッドも扱えるし、status
以外の値を設定することも勿論できる。
それらの使い方は公式ドキュメントに詳しく書かれている。