前回、ブラウザ向けのJavaScript ES6コードをJestでTDDする方法を紹介しました。
今回はcar.jsにfetch APIを導入してテストします。
なぜMockしないのか?
単体試験する際は普通はfetch APIをMockすると思います。
fetch APIをMockしない場合、デメリットが色々あります。
それでも今回はあえてMockなしで単体試験をしてみたいと思います。
まずはfetch API自体の理解を深めるためです。
次にfetch APIのようなAPIをJestで試験する手法の理解を深めるためです。
REQ|RES
REQRESはテスト用APIを公開しているサイトです。
今回はこのサイトのAPIを渋滞交通情報APIに見立てて叩きます。
テスト1:データ取得成功
まずはデータ取得に成功したケースのテストを書いてみましょう。
car.test.js
car.test.jsに記載する単体試験は以下の通りです。
Car#getTrafficDataはresolveされたPromiseを返す前提です。
const apiServer = 'https://reqres.in'; <async/awaitの場合> it('should get traffic data 1', async () => { const traffic = await car.getTrafficData(apiServer + '/api/user/2'); expect(traffic).toBe(2); }); <Promise/thenの場合> it('should get traffic data 2', () => { return car.getTrafficData(apiServer + '/api/user/2').then((traffic) => { expect(traffic).toBe(2); }); });
Promise/thenの場合はreturnを忘れてはいけません。忘れるとJestがresolveを待たずに進んでしまい、常にPassしてしまいます。
car.js
car.jsにgetTrafficDataを実装します。
getTrafficData() { const requestOptions = { method: 'GET', redirect: 'follow' }; return fetch("https://reqres.in/api/user/2", requestOptions) .then(response => response.json()) .then(result => Promise.resolve(result.data.id)); }
テスト実行
コーディング完了です。
「npm test」でテストを実行します。
するとエラーが発生してしまいました。
ReferenceError: fetch is not defined
「fetchがない」と言っています。
しかしfetch APIはブラウザが標準で実装しているAPIです。なぜエラーが出るのでしょう?
node-fetchを入れる
Jestを実行しているのはnode.jsです。そしてnode.jsにはfetch APIは標準ではないのでテストでエラーが出ています。
テスト用にnode-fetchをインストールします。
npm install --save-dev node-fetch
あくまでテスト用なので--save-devオプションで入れます。
require('node-fetch')を書く
次に「const fetch = require('node-fetch')」を書きます。
でもcar.jsにrequire文は書きたくありません。car.jsはブラウザ環境で使用する前提です。require文を書いたら今度はブラウザ環境でエラーが出てしまいます。それにブラウザには標準でfetch APIが備わっているのでnode-fetchは不要です。
そこでcar.test.jsに以下のように記述してglobalなfetchを上書きします。
global.fetch = require('node-fetch');
これでテスト実行時はcar.jsのfetchはnode-fetchになります。グローバル変数は恐ろしいけど便利です。。。
テスト実行
「npm test」でテストを実行します。
今度は無事、Passしました。
テスト2:エラー応答受信
サーバからエラー応答を受信したケースをテストします。
car.test.js
car.test.jsに記載する単体試験は以下の通りです。Car#getTrafficDataはrejectされたPromiseを返す前提です。
実際のコーディングにおいては空のPromiseを返す方が便利な気もしますがここではrejectされたケースを扱う勉強としてあえてrejectします。
<async/awaitの場合> it('should reject when 4XX is returned 1', async () => { expect.assertions(1); try { await car.getTrafficData('/api/user/23'); } catch (error) { expect(error).toBe('Error 404'); } }); <Promise/thenの場合> it('should reject when 4XX is returned 2', () => { return expect(car.getTrafficData('/api/user/23')).rejects.toBe('Error 404'); });
ここでもPromise/thenの場合はreturnを忘れてはいけません。よく忘れます。
新たな注意点ですが、async/awaitの場合、expect.assertions(1)でassertionが発生した回数をチェックすることを忘れないようにしましょう。これがないとresolveされたPromiseが帰ってきたときテストがPassしてしまいます。
ただ私の環境ではexpect.assertionsの動きがちょっとおかしいです。他のテストのassertionの回数まで数えてしまい他のテストの内容によってPass/Failが変わってしまいます。例えば他のテストでもrejectが戻ってきた場合、assertionの回数が2だと思ってしまいます。テストごとにassertionの回数をクリアする方法を探したのですがわかりませんでした。
car.js
getTrafficDataを実装を変更します。
fetch APIはサーバがエラーを返信してもresolveしたPromiseを返します。このため変更するのはcatchではありません。thenの中でresponse.okでエラー有無を確認して処理を分ける必要があります。
getTrafficData(resourcePath) { const requestOptions = { method: 'GET', redirect: 'follow' }; return fetch(`https://reqres.in${resourcePath}`, requestOptions) .then(response => { if (!response.ok) { return Promise.reject('Error 404'); } else { return response.json(); } }) .then(result => Promise.resolve(result.data.id)); }
テスト3:fetch APIのエラー
fetch API自身の処理でエラーが発生したケースをテストします。
car.test.js
car.test.jsに記載する単体試験は以下の通りです。
Car#getTrafficDataはrejectされたPromiseを返す前提です。
<async/awaitの場合> it('should reject when error happend 1', async () => { expect.assertions(1); try { await car.getTrafficData('http://dont.exist.api/api/bogus'); } catch (error) { expect(error).toMatch('message'); } }); <Promise/thenの場合> it('should reject when error happend 2', () => { return expect(car.getTrafficData('http://dont.exist.api/api/bogus')).rejects.toMatch('message'); });
car.js
getTrafficDataを実装を変更します。
今までの実装でもgetTrafficDataはrejectされたPromiseを返します。しかしfetch APIでエラーが発生したときPromiseの中身はオブジェクト形式です。サーバがエラー応答したときは文字列型、fetch APIエラーのときはオブジェクト型を返すという仕様はgetTrafficDataを呼び出す側にとって面倒なのでcatchの中で文字列に統一しました。
(もっといい方法があると思います)
getTrafficData(url) { const requestOptions = { method: 'GET', redirect: 'follow' }; return fetch(url, requestOptions) .then(response => { if (!response.ok) { return Promise.reject('Error 404'); } else { return response.json(); } }) .then(result => Promise.resolve(result.data.id)) .catch((error) => { if(typeof error === 'object') { return Promise.reject(JSON.stringify(error)); } else { return Promise.reject(error); } }); }
Mock
次は今回のテストをMockを使って行います。