プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

JavaScript ES6 + JestでTDD(fetch API Mockなし編)

前回、ブラウザ向けのJavaScript ES6コードをJestでTDDする方法を紹介しました。

architecting.hateblo.jp

今回はcar.jsにfetch APIを導入してテストします。


なぜMockしないのか?

単体試験する際は普通はfetch APIをMockすると思います。

fetch APIをMockしない場合、デメリットが色々あります。

  • 試験が遅くなる
  • サーバに負荷がかかる
  • サーバが到達不能な場合、試験ができない
  • 応答が変化するAPIの場合、Failが頻発してしまう

それでも今回はあえてMockなしで単体試験をしてみたいと思います。

まずはfetch API自体の理解を深めるためです。

次にfetch APIのようなAPIをJestで試験する手法の理解を深めるためです。


REQ|RES

REQRESはテスト用APIを公開しているサイトです。

https://reqres.in/

今回はこのサイトの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を使って行います。

architecting.hateblo.jp