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

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

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

前回、ブラウザ向けのJavaScript ES6コードにfetch APIを導入し、MockなしでTDDする方法を紹介しました。

architecting.hateblo.jp

今回は同じテストをMockで行う方法を紹介します。

なお今回はcar.js側の変更は一切ありません。変更されるのはテストだけです。


なぜMockするのか?

前回の繰り返しになりますがfetch APIをMockしない場合、デメリットが色々あります。

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

ちなみに個人的にはMockするときはMockライブラリを利用せず、自分でテスト用Mockを作成してInjectする方が好きです。


テスト1:データ取得成功

まずはデータ取得に成功したケースのテストを書き換えてみましょう。


carMock.test.js

前回はglobalなfetchをcar.test.js内でnode-fetchに上書きしました。

今回はglobalなfetchをMockで上書きします。

一つのjsファイル内でglobalなfetchを何度も上書きすると思ったような挙動にならないことがありました。そこでシンプルにファイルを分けることにします。

test/carMock.test.jsファイルを作成します。

carMock.test.jsに記載する単体試験は以下の通りです。

import Car from '../car';
global.fetch = jest.fn();

describe('CarMock', () => {
    const apiServer = 'https://reqres.in';
    let car = null;
    beforeEach(() => {
        car = new Car();
        global.fetch.mockClear();
    });

<async/awaitの場合>
    it('should get traffic data by fetch mock 1', async () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.resolve({
                ok: true,
                json: () => {
                    return Promise.resolve({
                        data: {
                            id: 2
                        }
                    })
                }
            });
        });
        const traffic = await car.getTrafficData(apiServer + '/api/user/2');
        expect(traffic).toBe(2);
        expect(global.fetch).toHaveBeenCalledTimes(1);

    });
<Promise/thenの場合>
    it('should get traffic data by fetch mock 2', () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.resolve({
                ok: true,
                json: () => {
                    return Promise.resolve({
                        data: {
                            id: 2
                        }
                    })
                }
            });
        });
        return car.getTrafficData(apiServer + '/api/user/2').then((traffic) => {
            expect(traffic).toBe(2);
            expect(global.fetch).toHaveBeenCalledTimes(1);

        });
    });

大半は前回と同様です。変更した箇所だけ解説します。

「global.fetch = require('node-fetch')」は不要です。

「global.fetch = jest.fn();」でglobalなfetchをJestのMockで上書きしています。これでテスト実行時はcar.js内のfetchもMockに上書きされます。

「global.fetch.mockImplementationOnce」でテストごとのMockの挙動を定義しています。挙動を定義するにあたっては前回、Mockなしテストで得た知識を活用しました。resolveされたPromiseはokとjson関数を持っている必要があります。

Mockの挙動の定義は「global.fetch = jest.fn(() => { ... });」で行うこともできます。しかしこの方法だとテストごとに挙動を切り替えることができない場面があり、試行錯誤の結果、上記の形に落ち着きました。

「expect(global.fetch).toHaveBeenCalledXXX」で色々Spyできます。

呼び出し回数等のMock情報はテストごとにクリアされません。このためbeforeEachの中で「global.fetch.mockClear()」しています。


テスト2:エラー応答受信

サーバからエラー応答を受信したケースのテストを変更します。


carMock.test.js

carMock.test.jsに記載する単体試験は以下の通りです。

「global.fetch.mockImplementationOnce」でMockの挙動を変更しています。Mockは「ok = false」を持つresolveされたPromiseを返します。

<async/awaitの場合>
    it('should reject when 4XX is returned 1', async () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.resolve({
                ok: false
            })
        });
        expect.assertions(1);
        try {
            await car.getTrafficData(apiServer + '/api/user/23');
        } catch (error) {
            expect(error).toBe('Error 404');
        }
    });
<Promise/thenの場合>
    it('should reject when 4XX is returned 2', () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.resolve({
                ok: false
            })
        });
        return expect(car.getTrafficData(apiServer + '/api/user/23')).rejects.toBe('Error 404');
    });

テスト3:fetch APIのエラー

fetch API自身の処理でエラーが発生したケースのテストを変更します。


carMock.test.js

carMock.test.jsに記載する単体試験は以下の通りです。

「global.fetch.mockImplementationOnce」でMockの挙動を変更しています。MockはrejectされたPromiseを返します。

<async/awaitの場合>
    it('should reject when error happend 1', async () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.reject({
                message: 'error message',
                result: 'description of the error'
            });
        });
        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', () => {
        global.fetch.mockImplementationOnce(() => {
            return Promise.reject({
                message: 'error message',
                result: 'description of the error'
            });
        });
        return expect(car.getTrafficData('http://dont.exist.api/api/bogus')).rejects.toMatch('message');
    });