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以外の値を設定することも勿論できる。
それらの使い方は公式ドキュメントに詳しく書かれている。