かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その43 )( Jest で jQuery.ajax の処理のテストを書く )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その42 )( Form.js のテストを Jest で書く2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Jest で jQuery.ajax($.ajax)の処理のテストを書きます。
    • setTimeout の処理のテストは Jest の 22.0 以降から使用可能になる jest.advanceTimersByTime を使ってみたいので、IntelliJ IDEA を 2017.3.2 にバージョンアップしてから記述します。

参照したサイト・書籍

  1. Jest - An Async Example
    https://facebook.github.io/jest/docs/en/tutorial-async.html

  2. Windowsでnpm installしてnode-gypでつまずいた時対処方法
    https://qiita.com/AkihiroTakamura/items/25ba516f8ec624e66ee7

  3. nodejs/node-gyp
    https://github.com/nodejs/node-gyp

  4. JavaScript Promiseの本
    http://azu.github.io/promises-book/

  5. How to fake jquery.ajax() response?
    https://stackoverflow.com/questions/5272698/how-to-fake-jquery-ajax-response

  6. jakerella / jquery-mockjax
    https://github.com/jakerella/jquery-mockjax/

目次

  1. Jest を 21.2.1 → 22.0.4 へバージョンアップ。。。は IntelliJ IDEA 2017.3.2 待ちでした
  2. $.ajax の処理のテストを書く
    1. テストで使用する API 呼び出し用のモジュールを作成する
    2. テストを書いて実行する
    3. $.ajax のモックを作成してテストする
    4. jquery-mockjax でサーバ側をモックにしてテストする
  3. 次回は。。。

手順

Jest を 21.2.1 → 22.0.4 へバージョンアップ。。。は IntelliJ IDEA 2017.3.2 待ちでした

setTimeout のテストで jest.advanceTimersByTime を使用してみたいので、Jest を最新の 22.0.4 へバージョンアップします。

今回は Jest だけバージョンアップしますので、npm update ではなく npm install --save-dev jest@22.0.4 コマンドを実行します。

f:id:ksby:20171223211148p:plain

Error: Can't find Python executable "python", you can set the PYTHON env variable. というエラーメッセージが表示されました。。。 PC に Python はインストールしていないのですが、Jest の 22.0 以降から必要になったのでしょうか?

調べると Windowsでnpm installしてnode-gypでつまずいた時対処方法 という記事を見つけました。また nodejs/node-gyp を見ると「管理者として実行...」で起動した PowerShell か CMD.exe から npm install --global --production windows-build-tools コマンドを実行すればよいようです。

CMD.exe を「管理者として実行...」で起動した後、npm install --global --production windows-build-tools コマンドを実行してみます。

f:id:ksby:20171224213036p:plain

インストールは成功しました。再度 npm install --save-dev jest@22.0.4 を実行してみます。

f:id:ksby:20171224213350p:plain

こちらも成功しました。

Form.test.js のテストを実行してみます。。。が、失敗しました。No tests found というメッセージが出ています。なぜかテストが見つからないようです。

f:id:ksby:20171224224335p:plain

いろいろ調べてみましたが、おそらく WEB-30087 Jest: unable to run single symlinked test file のことでしょう。IntelliJ IDEA の Bug で次のバージョン 2017.3.2 で修正されるようです。

npm install --save-dev jest@21.2.1 コマンドを実行して元に戻すことにします。Form.test.js のテストも成功することを確認します。

f:id:ksby:20171224225134p:plain

$.ajax の処理のテストを書く

テストで使用する API 呼び出し用のモジュールを作成する

郵便番号検索API を呼び出すモジュールを作成してみます。src/main/assets/js/lib/util の下に ZipcloudApiHelper.js というファイルを新規作成し、以下の内容を記述します。

"use strict";

module.exports = {

    /**
     * 郵便番号検索API を呼び出して、郵便番号から都道府県・市区町村・町域を取得する
     * @param {string} zipcode - 郵便番号(7桁の数字、ハイフン付きでも可)
     */
    search: function (zipcode) {
        var defer = $.Deferred();
        $.ajax({
            type: "get",
            url: "http://zipcloud.ibsnet.co.jp/api/search",
            data: {zipcode: zipcode},
            cache: false,
            dataType: "jsonp",
            timeout: 15000,
            success: defer.resolve,
            error: defer.reject
        });
        return defer.promise();
    }

};

テストを書いて実行する

src/test/assets/tests/lib/util の下に ZipcloudApiHelper.test.js というファイルを新規作成し、以下の内容を記述します。

"use strict";

global.$ = require("jquery");
const zipcloudApiHeler = require("lib/util/ZipcloudApiHelper.js");

describe("ZipcloudApiHelper.js のテスト", () => {

    test("100-0005で検索すると東京都千代田区丸の内が1件ヒットする", () => {
        zipcloudApiHeler.search("1000005")
            .then(json => {
                expect(json.results.length).toBe(1);
            })
            .catch(e => {
                console.log(e);
            });
    });

});

テストを実行すると成功しました。

f:id:ksby:20171224231710p:plain

非同期処理もテストが出来るものなんだなあと思いましたが、breakpoint を設置して本当にコードを実行しているか確認してみます。

f:id:ksby:20171224232012p:plain

が、全く止まりませんでした。API を呼び出しているだけで、then と catch のどちらのコードも実行されていません。テストになっていませんね。。。

Jest の An Async Example のページを読んでみると async / await を使えば良さそうです。src/test/assets/tests/lib/util/ZipcloudApiHelper.test.js を以下のように変更して試してみます。

    test("100-0005で検索すると東京都千代田区丸の内が1件ヒットする", async () => {
        const json = await zipcloudApiHeler.search("1000005");
        expect(json.results.length).toBe(1);
        expect(json.results[0].address1).toBe("東京都");
        expect(json.results[0].address2).toBe("千代田区");
        expect(json.results[0].address3).toBe("丸の内");
    });

テストを実行すると成功しました。

f:id:ksby:20171226005119p:plain

breakpoint を設置して Debug 実行してみると、expect のところで処理も止まり変数 json の中にデータが入っていることが確認できます。

f:id:ksby:20171226005551p:plain

ちなみに async / await を削除するとテストは失敗します。

f:id:ksby:20171226010305p:plain

$.ajax のモックを作成してテストする

stackoverflow の How to fake jquery.ajax() response? という QA を見つけました。ajax_response という関数を定義してモック化できるようです。

以下のテストを src/test/assets/tests/lib/util/ZipcloudApiHelper.test.js に追加します。

    describe("$.ajax のモックを作成してテストする", () => {

        /**
         * $.ajax モック用関数
         * https://stackoverflow.com/questions/5272698/how-to-fake-jquery-ajax-response 参照
         */
        function ajax_response(response, success) {
            return function (params) {
                if (success) {
                    params.success(response);
                } else {
                    params.error(response);
                }
            };
        }

        test("success のテスト", async () => {
            $.ajax = ajax_response({
                results: [
                    {address1: "東京都"},
                    {address1: "神奈川県"}
                ]
            }, true);
            const json = await zipcloudApiHeler.search("1000005");
            expect(json.results.length).toBe(2);
            expect(json.results[0].address1).toBe("東京都");
            expect(json.results[1].address1).toBe("神奈川県");
        });

        test("error のテスト", async () => {
            $.ajax = ajax_response({error: "エラー"}, false);
            await expect(zipcloudApiHeler.search("1000005")).rejects.toEqual({error: "エラー"});
        });

    });

テストを実行すると成功しました。

f:id:ksby:20171226014601p:plain

jquery-mockjax でサーバ側をモックにしてテストする

サーバ側をモックにして正常にデータが返る場合や 500 等の HTTPステータスコードが返ってくる場合のテストをしたいと思い、調べてみたのですが、以外に難しい。。。

  • API を呼び出す場合、ブラウザ環境と Node.js 環境で異なるらしい。そんな違いがあるんだ、と少し驚きました。
  • ブラウザ環境は XMLHttpRequests を使用する。
  • Node.js 環境は http requests モジュールを使用する。
  • よく聞く axios だと環境に応じて XMLHttpRequests を使用するか http requests を使用するか判断してくれるらしい。
  • 今回は $.ajax のテストをしたいので、XMLHttpRequests をモックにしてくれるライブラリが欲しい。
  • Jest には XMLHttpRequests をモックにする機能はなさそう。
  • Sinon.JS Fake XHR and server を見ると sinon.useFakeXMLHttpRequest()sinon.fakeServer を使えば出来そうな気がする。ただし Sinon.JS は機能が結構豊富なので、Jest を使っているのに $.ajax のテストだけのために Sinon.JS を入れることに少し抵抗あり。もう少し機能を絞ったライブラリでも良さそう。
  • node-nock / nock というモジュールを発見したが、これは Node.js 環境で http requests をモックにするモジュールらしい。
  • 他に何かないか探してみたところ jakerella / jquery-mockjax というモジュールを発見。試してみたら Node.js 環境でも動作したので、これにしてみます!

npm install --save-dev jquery-mockjax コマンドを実行してインストールします。

f:id:ksby:20171228135423p:plain

以下のテストを src/test/assets/tests/lib/util/ZipcloudApiHelper.test.js に追加します。

"use strict";

global.$ = require("jquery");
const zipcloudApiHeler = require("lib/util/ZipcloudApiHelper.js");
const mockjax = require("jquery-mockjax")(global.$, window);

describe("ZipcloudApiHelper.js のテスト", () => {

    ..........

    describe("jquery-mockjax でサーバ側をモックにして $.ajax をテストする", () => {

        afterEach(() => {
            mockjax.clear();
        });

        test("データを返す場合のテスト", async () => {
            mockjax({
                url: "http://zipcloud.ibsnet.co.jp/api/search",
                responseText: {
                    results: [
                        {address1: "東京都"},
                        {address1: "神奈川県"}
                    ]
                }
            });

            const json = await zipcloudApiHeler.search("1000005");
            expect(json.results.length).toBe(2);
            expect(json.results[0].address1).toBe("東京都");
            expect(json.results[1].address1).toBe("神奈川県");
        });

        // dataType: "jsonp" だとこのテストは成功しない
        // status: 500 にしても reject が呼び出されず、resolve が呼び出される
        test("HTTPステータスコード 500 が返ってくる場合のテスト", async () => {
            mockjax({
                url: "http://zipcloud.ibsnet.co.jp/api/search",
                status: 500
            });

            await expect(zipcloudApiHeler.search("1000005"))
                .rejects.toHaveProperty("status", 500);
        })

        // dataType: "jsonp" だとこのテストは成功しない
        // isTimeout: true にしても reject が呼び出されず、resolve が呼び出される
        test("Timeout する場合のテスト", async () => {
            mockjax({
                url: "http://zipcloud.ibsnet.co.jp/api/search",
                isTimeout: true
            });

            await expect(zipcloudApiHeler.search("1000005"))
                .rejects.toHaveProperty("statusText", "timeout");
        })

    });

});

コメントに書きましたが、$.ajax を呼び出す時に dataType: "jsonp" を記述しているとエラー系(HTTPステータスコード 500 を返す、Timeout させる)が全く成功しません。テストを実行すると最初のデータを返す場合しか成功しませんでした。

f:id:ksby:20171229082835p:plain

src/main/assets/js/lib/util/ZipcloudApiHelper.js で dataType: "jsonp"コメントアウトすると、

"use strict";

module.exports = {

    /**
     * 郵便番号検索API を呼び出して、郵便番号から都道府県・市区町村・町域を取得する
     * @param {string} zipcode - 郵便番号(7桁の数字、ハイフン付きでも可)
     */
    search: function (zipcode) {
        var defer = $.Deferred();
        $.ajax({
            type: "get",
            url: "http://zipcloud.ibsnet.co.jp/api/search",
            data: {zipcode: zipcode},
            cache: false,
            // dataType: "jsonp",
            timeout: 15000,
            success: defer.resolve,
            error: defer.reject
        });
        return defer.promise();
    }

};

テストは全て成功するようになります。

f:id:ksby:20171229083313p:plain

jakerella / jquery-mockjax$.ajax のテストをするにはかなり使い勝手いいですね。ただし dataType: "jsonp" を指定した場合には正常系しかテストは成功しないので要注意です。

次回は。。。

IntelliJ IDEA の 2017.3.2 が出たので、2017.3.2 にバージョンアップした後、Jest を 22.0 以降にバージョンアップしてから setTimeout の処理のテストを書きます。

履歴

2017/12/29
初版発行。