かんがるーさんの日記

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

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • Jest で setTimeout の処理のテストを書きます。

参照したサイト・書籍

  1. Jest - Timer Mocks
    https://facebook.github.io/jest/docs/en/timer-mocks.html

目次

  1. Jest を 21.2.1 → 22.0.4 へバージョンアップする。。。は IntelliJ IDEA 2017.3.3 待ちでした
  2. setTimeout の処理のテストを書く
    1. テストで使用するモジュールを作成する
    2. テストを書いて実行する
  3. 次回は。。。

手順

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

IntelliJ IDEA を 2017.3.2 へバージョンアップしたので、改めて npm install --save-dev jest@22.0.4 コマンドを実行して Jest を 22.0.4 へバージョンアップします。

f:id:ksby:20171229160345p:plain

Form.test.js のテストを実行してみます。。。が、失敗しました。前と同じ No tests found というメッセージが出ますね?

f:id:ksby:20171229175716p:plain

もしかしてまだ修正されていないのかと思い、IntelliJ IDEA の次の EAP のページを IDEA 2017.3 EAPIntelliJ IDEA 2017.3 173.4301.1 Release Notes と見てみると、Bug WEB-30377 wrong test pattern when running single Jest test, 'no tests found' が見つかりました。どうも修正されるのは次の 2017.3.3 のようです。

npm install --save-dev jest@21.2.1 コマンドを実行して元に戻すことにします。

また今回試している時に気づきましたが、Java のテストと同様に Ctrl+Shift+T を押すとテスト対象のモジュールのファイルとテストのファイルの間を行ったり来たりできました。

setTimeout の処理のテストを書く

テストで使用するモジュールを作成する

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

"use strict";

let id = undefined;

module.exports = {

    /**
     * 指定された時間経過後に実行する関数を登録する
     * @param {Function} fn - 遅延実行する関数
     * @param {number} milliSeconds - 関数 fn を遅延実行する時間(ミリ秒)
     */
    register: function (fn, milliSeconds) {
        if (id !== undefined) {
            this.cancel();
        }
        id = setTimeout(fn, milliSeconds);
    },

    /**
     * 登録されている遅延実行関数をキャンセルする
     */
    cancel: function () {
        if (id !== undefined) {
            clearTimeout(id);
            id = undefined;
        }
    }

};

テストを書いて実行する

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

"use strict";

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

jest.useFakeTimers();

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

    function setSampleValue() {
        $("#sample").val("サンプル");
    }

    function setTestValue() {
        $("#sample").val("テスト");
    }

    beforeEach(() => {
        document.body.innerHTML = `
            <input type="text" name="sample" id="sample" value="">
        `;
    });

    test("delayExecutor.register で関数を登録すると指定した時間経過後に実行される", () => {
        delayExecutor.register(setSampleValue, 2000);
        expect($("#sample").val()).toBe("");
        jest.runAllTimers();
        expect($("#sample").val()).toBe("サンプル");
    });

    test("delayExecutor.cancel を呼び出せば指定した時間を経過しても実行されない", () => {
        delayExecutor.register(setSampleValue, 2000);

        // ここで jest.advanceTimersByTime(...) を呼び出して、
        // 少しだけ時間を経過させたかった。。。

        delayExecutor.cancel();
        expect($("#sample").val()).toBe("");
        jest.runAllTimers();
        expect($("#sample").val()).toBe("");
    });

    test("delayExecutor.register は最後に登録した関数だけが実行される", () => {
        delayExecutor.register(setSampleValue, 2000);
        delayExecutor.register(setTestValue, 1000);

        expect($("#sample").val()).toBe("");
        jest.runAllTimers();
        expect($("#sample").val()).toBe("テスト");
    });

});

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

f:id:ksby:20171230020511p:plain

やっぱり setTimeout のテストを書くなら jest.advanceTimersByTime が使いたいかな。。。

次回は。。。

番外編で以下の3つを書いてみる予定です。

  • axiosNock でテストを書いてみる
  • MobX を使ってみる(このモジュールを使えば jQuery でも状態管理ができるらしいのでちょっと興味が湧きました)
  • Flow を使ってみる(Javascript を書いていると型が欲しいと切実に思いますが、これか Typescript を使えば型がある書き方ができるらしいので試してみます)

その後に Gradle の build タスクで Javascript の build +テストを一緒に実行する方法を調べてから、入力画面3以降の作成に移る予定です。

履歴

2017/12/30
初版発行。

IntelliJ IDEA を 2017.3.1 → 2017.3.2 へ、Git for Windows を 2.15.0 → 2.15.1(2) へバージョンアップ

IntelliJ IDEA を 2017.3.1 → 2017.3.2 へバージョンアップする

IntelliJ IDEA の 2017.3.2 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20171229095908p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20171229100104p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20171229100619p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.3.2 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20171229100940p:plain

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20171229101658p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

f:id:ksby:20171229102120p:plain

Git for Windows を 2.15.0 → 2.15.1(2) へバージョンアップする

Git for Windows の 2.15.1(2) がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.15.1.2-64-bit.exe をダウンロードします。

  2. Git-2.15.1.2-64-bit.exe を実行します。

  3. 「Git 2.15.1.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Choosing the default editor used by Git」画面が表示されます(新オプションですね)。中央の「Use Vim (the ubiquitous text editor) as Git's default editor」が選択されていることを確認後、[Next >]ボタンをクリックします。

    nano って何? と思って調べるとコンソールで動くテキストエディタで、Ubuntu だとデフォルトが nano らしい。nanoエディタユーザーじゃない人がうっかりnanoエディタを開いてしまった時の対処法 とかの記事がヒットするのですが、コマンドラインで Git を使うような人が Vim ではなく nano を使うなんてあるのかな。。。

    WindowsVim を使わないならば GitBashで起動するテキストエディタの変更 の記事のように sakura editor に変えてしまった方が便利な気がします。

  6. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  8. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  10. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  11. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  12. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.15.1.windows.2 になっていることを確認します。

    f:id:ksby:20171229122725p:plain

  13. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20171229122847p:plain

  14. 特に問題はないようですので、2.15.1(2) で作業を進めたいと思います。

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
初版発行。

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

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その41 )( IntelliJ IDEA で Javascript を debug する ) の続きです。

参照したサイト・書籍

  1. jQuery - Category: Event Object
    https://api.jquery.com/category/events/event-object/

目次

  1. class 属性を変更するメソッドのテストを書く
    1. resetValidation メソッド
    2. setSuccess メソッド
    3. setError メソッド
  2. Validation 用メソッドのテストを書く
    1. convertAndValidate メソッド
  3. IntelliJ IDEA の 2017.3 から coverage をエディタ上に表示できる
  4. 次回は。。。

手順

class 属性を変更するメソッドのテストを書く

resetValidation メソッド

以下のテストを書いて、

    describe("class 属性を変更するメソッドのテスト", () => {

        describe("resetValidation メソッドのテスト", () => {

            test("入力チェック成功時(has-success)は has-success が削除される", () => {
                document.body.innerHTML = `
                    <div class="form-group has-success" id="form-group-name">
                      <div class="control-label col-sm-2">
                        <label class="float-label">お名前</label>
                      </div>
                      <div class="col-sm-10">
                        <div class="row"><div class="col-sm-10">
                          <input type="text" name="lastname" id="lastname" class="form-control form-control-inline"
                                value="" placeholder="例)田中"/>
                        </div></div>
                        <div class="row hidden js-errmsg"><div class="col-sm-10">
                          <p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p>
                        </div></div>
                      </div>
                    </div>
                `;

                let form = new Form([]);
                expect($("#form-group-name").hasClass("has-success")).toBe(true);
                expect($(".js-errmsg").hasClass("hidden")).toBe(true);
                form.resetValidation("#form-group-name");
                expect($("#form-group-name").hasClass("has-success")).toBe(false);
                expect($(".js-errmsg").hasClass("hidden")).toBe(true);
            });

            test("入力チェックエラー時(has-error)は has-error が削除されてエラーメッセージが非表示になる", () => {
                document.body.innerHTML = `
                    <div class="form-group has-error" id="form-group-name">
                      <div class="control-label col-sm-2">
                        <label class="float-label">お名前</label>
                      </div>
                      <div class="col-sm-10">
                        <div class="row"><div class="col-sm-10">
                          <input type="text" name="lastname" id="lastname" class="form-control form-control-inline"
                                value="" placeholder="例)田中"/>
                        </div></div>
                        <div class="row js-errmsg"><div class="col-sm-10">
                          <p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p>
                        </div></div>
                      </div>
                    </div>
                `;

                let form = new Form([]);
                expect($("#form-group-name").hasClass("has-error")).toBe(true);
                expect($(".js-errmsg").hasClass("hidden")).toBe(false);
                form.resetValidation("#form-group-name");
                expect($("#form-group-name").hasClass("has-error")).toBe(false);
                expect($(".js-errmsg").hasClass("hidden")).toBe(true);
            });

        });

    });

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

f:id:ksby:20171220012914p:plain

setSuccess メソッド

以下のテストを書いて、

        test("setSuccess メソッドのテスト", () => {
            document.body.innerHTML = `
                <div class="form-group" id="form-group-name">
                </div>
            `;

            let form = new Form([]);
            expect($("#form-group-name").hasClass("has-success")).toBe(false);
            form.setSuccess("#form-group-name");
            expect($("#form-group-name").hasClass("has-success")).toBe(true);
        });

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

f:id:ksby:20171220014053p:plain

setError メソッド

以下のテストを書いて、

        test("setError メソッドのテスト", () => {
            document.body.innerHTML = `
                <div class="form-group" id="form-group-name">
                  <div class="control-label col-sm-2">
                    <label class="float-label">お名前</label>
                  </div>
                  <div class="col-sm-10">
                    <div class="row"><div class="col-sm-10">
                      <input type="text" name="lastname" id="lastname" class="form-control form-control-inline"
                            value="" placeholder="例)田中"/>
                    </div></div>
                    <div class="row hidden js-errmsg"><div class="col-sm-10">
                      <p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p>
                    </div></div>
                  </div>
                </div>
            `;

            let form = new Form([]);
            expect($("#form-group-name").hasClass("has-error")).toBe(false);
            expect($(".js-errmsg").hasClass("hidden")).toBe(true);
            expect($(".js-errmsg small").text()).toBe("ここにエラーメッセージを表示します");
            form.setError("#form-group-name", "お名前を入力してください");
            expect($("#form-group-name").hasClass("has-error")).toBe(true);
            expect($(".js-errmsg").hasClass("hidden")).toBe(false);
            expect($(".js-errmsg small").text()).toBe("お名前を入力してください");
        });

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

f:id:ksby:20171220014840p:plain

Validation 用メソッドのテストを書く

convertAndValidate メソッド

以下のテストを書いて、

    describe("Validation 用メソッドのテスト", () => {

        describe("convertAndValidate メソッドのテスト", () => {

            const idFormGroup = "#form-group-name";
            const idList = ["#name", "#age"];

            test("全ての要素に focus していると converter, validator が呼ばれる", () => {
                let form = new Form(idList);
                let event = $.Event("test");
                const converter = jest.fn();
                const validator = jest.fn();

                form.forceAllFocused(form);
                form.convertAndValidate(form, event, idFormGroup, idList, converter, validator);
                expect(converter).toBeCalled();
                expect(validator).toBeCalled();
                expect(event.isPropagationStopped()).toBe(false);
            });

            test("全ての要素に focus していないと converter, validator は呼ばれない", () => {
                let form = new Form(idList);
                let event = $.Event("test");
                const converter = jest.fn();
                const validator = jest.fn();

                form.convertAndValidate(form, event, idFormGroup, idList, converter, validator);
                expect(converter).not.toBeCalled();
                expect(validator).not.toBeCalled();
                expect(event.isPropagationStopped()).toBe(false);
            });

            test("converter, validator に undefined を渡してもエラーにならない", () => {
                let form = new Form(idList);
                let event = $.Event("test");

                form.forceAllFocused(form);
                expect(() => {
                    form.convertAndValidate(form, event, idFormGroup, idList, undefined, undefined);
                }).not.toThrow();
                expect(event.isPropagationStopped()).toBe(false);
            });

            test("validator が Error オブジェクトを throw すると event.stopPropagation() が呼ばれる", () => {
                let form = new Form(idList);
                let event = $.Event("test");
                const converter = jest.fn();
                const validatorThrowError = jest.fn().mockImplementation(() => {
                    throw new Error();
                });

                form.forceAllFocused(form);
                form.convertAndValidate(form, event, idFormGroup, idList, converter, validatorThrowError);
                expect(event.isPropagationStopped()).toBe(true);
            });

        });

    });

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

f:id:ksby:20171221011404p:plain

IntelliJ IDEA の 2017.3 から coverage をエディタ上に表示できる

IntelliJ IDEA 2017.3 から Jest のテストを実行するコンテキストメニューに「Run ... with Coverage」が表示されるようになりました。また、このメニューからテストを実行すると Jest のレポートファイルを見なくてもエディタ上で実行された部分が分かります。

例えば「Run 'focus イベントに関するメソッドのテスト' with Coverage」を実行してみると、

f:id:ksby:20171222060252p:plain

テストが実行されて成功し、

f:id:ksby:20171222060938p:plain

Coverage の Window が表示されて、Form.js の 39% の行がテストされたことが分かります。

f:id:ksby:20171222061130p:plain f:id:ksby:20171222061300p:plain

Form.js をエディタで開いてみると、画面の左側にテストが実行された箇所が緑で、実行されていない箇所が赤で表示されます。

f:id:ksby:20171222061527p:plain

テストの実行された/されていない箇所を表示する色はデフォルトでは見にくい色だったので、単純な緑、赤に変更しています。色が表示されている部分にマウスカーソルを移動して左クリックした後、「Edit coverage colors」のアイコンをクリックします。

f:id:ksby:20171222061804p:plain

「Editor | Color Scheme | General」ダイアログが表示されるので、緑は 00FF00 へ、

f:id:ksby:20171222062055p:plain f:id:ksby:20171222062137p:plain

赤は FF0000 へ変更しています。

f:id:ksby:20171222062319p:plain f:id:ksby:20171222062410p:plain

次回は。。。

もう少し Jest でテストを書いてみます。jQuery.ajax 呼び出し時の非同期処理や、setTimeout で遅延して処理を実行する場合のテストを書く予定です。

それにしても IntelliJ IDEA 2017.3 + Jest の環境だと、Javascript のテストを書いたり実行したり、coverage を確認したり、うまく動かない時に debug 実行したりも、Java の時とたいして変わりがないですね。開発しやすい印象です。テスト実行時の Node.js のパラメータ指定(--inspect 等)も IntelliJ IDEA が自動でセットしてくれるのは便利です。

履歴

2017/12/22
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その41 )( IntelliJ IDEA で Javascript を debug する )

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 以前どこかで IntelliJ IDEA 2017.3 から Chrome の JetBrains IDE Support Extension をインストールせずに Javascript の debug が出来るようになるという記事を見かけたのですが、IntelliJ IDEA で Javascript の debug が出来るんだと思って今回バージョンアップしたので試してみたところ、すごい便利でした!
    • Javascript の debug は ChromeFirefox でやるものと思っていましたが、IntelliJ IDEA でやると Java と同じ画面で debug が出来るので、とても使いやすいです。この機能はオススメです!!
    • (2017/12/19) NodeJS Plugin のインストールと設定が必要でしたが、漏れていたので追記しました。

参照したサイト・書籍

  1. webpack - Devtool
    https://webpack.js.org/configuration/devtool/

  2. WebStorm 2017.3 Help - Debugging JavaScript in Chrome
    https://www.jetbrains.com/help/webstorm/debugging-javascript-in-chrome.html

目次

  1. NodeJS Plugin をインストールする
  2. Run/Debug Configurations から Attach to Node.js/Chrome と Node.js を設定する
  3. webpack.config.js の設定を変更してソースマップを出力する
  4. Tomcat と Browsersync を起動して入力画面1を表示する
  5. IntelliJ IDEA で input01.js を debug する
  6. 非同期のコードも debug できる
  7. Jest で実行するテストも debug できる

手順

NodeJS Plugin をインストールする

IntelliJ IDEA で Javascript を debug するには NodeJS Plugin が必須ですのでインストールします。

IntelliJ IDEA のメインメニューから「File」-「Settings...」を選択し「Settings」ダイアログを表示します。ダイアログが表示されたら、画面左側のツリーから「Plugins」を選択します。選択後、画面右側上部にある検索フィールドに nodejs と入力し Plugin がインストールされていなければ、画面下の「Browse repositories...」ボタンをクリックします。

f:id:ksby:20171219004949p:plain

「Browse Repositories」ダイアログが表示されます。画面左側のリストに表示されている「NodeJS」を選択し、「Install」ボタンをクリックします。

f:id:ksby:20171219005240p:plain

Plugin がダウンロードされ、完了すると「Install」ボタンが「Restart IntelliJ IDEA」ボタンに変わりますのでクリックします。

「Settings」ダイアログに戻ったら「OK」ボタンをクリックします。ダイアログが閉じると「IDE and Updates」ダイアログが表示されますので「Restart」ボタンをクリックして IntelliJ IDEA を再起動します。

Run/Debug Configurations から Attach to Node.js/Chrome と Node.js を設定する

IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択し、「Run/Debug Configurations」ダイアログを表示します。

最初に「Defaults」の下にある「Attach to Node.js/Chrome」を選択し、画面右側の「Attach to」の設定がインストールしている Node.js のバージョンと一致していることを確認します。

f:id:ksby:20171219011245p:plain

次に「Defaults」の下にある「Node.js」を選択し、画面右側の「Browser / Live Edit」タブをクリックします。

f:id:ksby:20171219011446p:plain

画面が切り替わったら「After launch」と「with JavaScript debugger」をチェックした後、「OK」ボタンをクリックしてダイアログを閉じます。

f:id:ksby:20171219011701p:plain

webpack.config.js の設定を変更してソースマップを出力する

まずはソースマップを出力します。出力しないと IntelliJ IDEA で debugger の画面は表示されても、ソースファイルの実行中の行を指し示してくれません。

webpack.config.js を以下のように変更します。

    ..........
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ],
    devtool: "inline-source-map"
};
  • devtool: "inline-source-map" を追加します。

Tomcat と Browsersync を起動して入力画面1を表示する

次に各サーバを起動します。まずは Tomcat を起動します(下の画面キャプチャは Debug with JRevel で起動しています)。

f:id:ksby:20171218011647p:plain

npm run springboot コマンドを実行して webpack や Browsersync を起動します。

f:id:ksby:20171218011942p:plain

Chrome を起動して http://localhost:9080/inquery/input/01/ にアクセスして画面が表示されることを確認します(この時の画面は動作確認をしているだけで debug 時には使用されません)。

f:id:ksby:20171218012441p:plain

IntelliJ IDEA で input01.js を debug する

IntelliJ IDEA 内で src/main/assets/js/inquiry/input01.js をエディタで開いた後、エディタ内でコンテキストメニューを表示して「Debug 'input01.js'」を選択します。

f:id:ksby:20171218012809p:plain

「Edit configuration」ダイアログが表示されますので、「Browser / Live Edit」タグをクリックして、

f:id:ksby:20171218013034p:plain

中央の入力フィールドに画面のURL(今回は http://localhost:9080/inquery/input/01/)を入力した後、「Debug」ボタンをクリックします。

f:id:ksby:20171218013240p:plain

IntelliJ IDEA の画面下に input01.js JavaScript の画面が表示され、

f:id:ksby:20171218013358p:plain

Chrome が起動して http://localhost:9080/inquiry/input/01/ にアクセスします。

f:id:ksby:20171218013608p:plain

この時に IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示すると、「JavaScript Debug」と「Node.js」が追加されています。ということは、Javascript の実行は Node.js で行っているのでしょう。

f:id:ksby:20171218013936p:plain f:id:ksby:20171218014032p:plain

debug してみます。まず input01.js の nameValidator メソッドに breakpoint をセットします。

f:id:ksby:20171218014233p:plain

入力画面1の「お名前(漢字)」の「姓」の入力フィールドにカーソルをセットしてから TAB キーを押して blur イベントを発生させます。

f:id:ksby:20171218014416p:plain

そうすると Chrome 上は「Paused in debugger」が表示されて、

f:id:ksby:20171218014549p:plain

IntelliJ IDEA の画面では breakpoint をセットした行で処理が止まります。また Java で debug した時と同様にエディタ上に変数の内容が表示されたり、画面の下に呼び出されたメソッドや変数とその値が表示されます。

f:id:ksby:20171218014747p:plain

「Step Over」ボタンをクリックすれば処理が先に進んで、新たに定義した変数が画面下に表示されますし、

f:id:ksby:20171218015200p:plain

form.convertAndValidate メソッドのところで「Step Into」ボタンを押せば、Form.js に移動して引き続き debug が出来ます。input01.js だけでしか debug できないということはないようです。

f:id:ksby:20171218015523p:plain

非同期のコードも debug できる

入力画面2には入力された郵便番号を使用して ajax で郵便番号検索APIを呼び出す処理を実装していますが、結果を受け取る非同期の処理の部分に breakpoint を設置しても debug ができます。

まず IntelliJ IDEA の画面下に表示されている「input01.js」と「input01.js JavaScript」の Window を左側にある×ボタンをクリックして閉じます。

次に IntelliJ IDEA 内で src/main/assets/js/inquiry/input02.js をエディタで開いた後、エディタ内でコンテキストメニューを表示して「Debug 'input02.js'」を選択します。

f:id:ksby:20171218021845p:plain

「Edit configuration」ダイアログが表示されますので、「Browser / Live Edit」タブをクリックしてから http://localhost:9080/inquiry/input/02/ を入力して「Debug」ボタンをクリックします。

f:id:ksby:20171218022123p:plain

今度は画面の下に input02.js JavaScript の画面が表示され、

f:id:ksby:20171218022435p:plain

Chromehttp://localhost:9080/inquiry/input/02/ にアクセスして入力画面2の画面が表示されます。

f:id:ksby:20171218022550p:plain

input02.js の ajax の結果を受け取る非同期の処理の部分に breakpoint を設置します。

f:id:ksby:20171218022749p:plain

Chrome 上で郵便番号を入力してカーソルを住所に移動します。そうすると「Paused in debugger」が表示されて、

f:id:ksby:20171218023001p:plain

IntelliJ IDEA の方では breakpoint の所で処理が止まります。

f:id:ksby:20171218023132p:plain

Jest で実行するテストも debug できる

Jest 用に書いたテストに breakpoint を設置して、

f:id:ksby:20171219012625p:plain

Debug 実行します。

f:id:ksby:20171219012738p:plain

テストが実行されて breakpoint のところで処理が止まります。画面の下には呼び出されたメソッドや、変数と値が表示されます。

f:id:ksby:20171219012856p:plain

「Step Over」をクリックすれば1つずつ処理を進めて動作を確認することができます。

f:id:ksby:20171219013206p:plain

履歴

2017/12/18
初版発行。
2017/12/19
以下の章を追記しました。
* NodeJS Plugin をインストールする
* Run/Debug Configurations から Attach to Node.js/Chrome と Node.js を設定する
* Jest で実行するテストも debug できる

IntelliJ IDEA を 2017.2.6 → 2017.3.1 へバージョンアップ

IntelliJ IDEA を 2017.2.6 → 2017.3.1 へバージョンアップする

IntelliJ IDEA の 2017.3 が少し前にリリースされましたが、HTML か JS のフォーマットが崩れたので 2017.2 に戻したという記事を見かけたのでバージョンアップしないでいました。2017.3.1 がリリースされて、さすがに解消されていると思うのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。今回は左下に「Update and Restart」ボタンが表示されていないので、

    f:id:ksby:20171216200358p:plain

    「Download」ボタンを押して IntelliJ IDEA の Download ページを開いた後、「DOWNLOAD」ボタンを押して ideaIU-2017.3.1.exe をダウンロードします。

    f:id:ksby:20171216200613p:plain

  3. 起動している IntelliJ IDEA を終了します。

  4. ideaIU-2017.3.1.exe を実行します。

  5. IntelliJ IDEA Setup」ダイアログが表示されます。「Next >」ボタンをクリックします。

    f:id:ksby:20171216203658p:plain

  6. 「Uninstall old versions」画面が表示されます。画面上の全てのチェックボックスをチェックした後、「Next >」ボタンをクリックします。

    f:id:ksby:20171216203849p:plain

  7. 「Choose Install Location」画面が表示されます。「Destination Folder」を C:\IntelliJ_IDEA\2017.3.1 に変更した後、「Next >」ボタンをクリックします。

    f:id:ksby:20171216204055p:plain

  8. 「Installation Options」画面が表示されます。何も変更せずに「Next >」ボタンをクリックします。

    f:id:ksby:20171216204149p:plain

  9. 「Choose Start Menu Folder」画面が表示されます。何も変更せずに「Install」ボタンをクリックします。

    f:id:ksby:20171216204259p:plain

  10. 「Installing」画面が表示されてインストールが始まりますので、完了するまで待ちます。

  11. インストールが完了すると「Completing IntelliJ IDEA Setup」画面が表示されます。「Finish」ボタンをクリックしてダイアログを閉じます。

    f:id:ksby:20171216204653p:plain

  12. C:\IntelliJ_IDEA\2017.2 ディレクトリが残っているので削除します。

  13. C:\IntelliJ_IDEA\2017.3.1\bin\idea64.exe を実行します。

  14. 「Complete Installation」ダイアログが表示されます。何も変更せずに「OK」ボタンをクリックします。

    f:id:ksby:20171216204945p:plain

  15. IntelliJ IDEA のメイン画面が表示され画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20171216205232p:plain

  16. 「Indexing…」のメッセージが消えると「Project code style settings migration」というダイアログが表示されました。

    f:id:ksby:20171216205710p:plain

    ダイアログ左下の「More info」のリンクをクリックすると https://confluence.jetbrains.com/display/IDEADEV/New+project+code+style+settings+format+in+2017.3 のページが表示されます。code style の設定が変わるようですが、一旦このままにしておきます。

    また上の画面キャプチャでは消えていますが、IntelliJ IDEA を再起動するダイアログも出ていたので再起動します。

  17. Plugin がアップデートされていないので先にアップデートします。IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  18. IDE and Plugin Updates」ダイアログが表示されます。何も変更せずに「Update」ボタンをクリックします。

    f:id:ksby:20171216210430p:plain

    Patch がダウンロードされた後、IntelliJ IDEA を再起動します。再起動後、画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

  19. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.3.1 へバージョンアップされていることを確認します。

  20. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20171216212021p:plain

  21. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20171216212746p:plain

  22. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20171216213239p:plain

  23. 2017.2 に戻るかもしれないので、C:\Users\root.IntelliJIdea2017.2 の削除は今回は保留します。

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

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その39 )( Spring Boot を 1.5.7 → 1.5.9 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Jest を使用して Javascript のモジュールのテストを書きます。
    • 今回は src/main/assets/js/lib/class/Form.js のテストを書きます。

参照したサイト・書籍

  1. How to duplicate object properties in another object?
    https://stackoverflow.com/questions/9362716/how-to-duplicate-object-properties-in-another-object

  2. Object.keys()
    https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/keys

目次

  1. テストを書くファイルを用意する
  2. focus イベントに関するメソッドのテストを書く
    1. setFocused メソッド
    2. isAllFocused メソッド
    3. setFocusedFromList メソッド
    4. forceAllFocused メソッド
    5. backupFocusedState, restoreFocusedState メソッド
  3. 値の入力有無をチェックするメソッドのテストを書く
    1. isAllEmpty メソッド
    2. isAnyEmpty メソッド
    3. isAnyNotEmpty メソッド
  4. 続きます。。。

手順

テストを書くファイルを用意する

src/test/assets/tests/lib の下に class ディレクトリを作成し、その下に Form.test.js を新規作成して以下の内容を記述します。

"use strict";

global.$ = require("jquery");
const Form = require("lib/class/Form.js");

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

    describe("focus イベントに関するメソッドのテスト", () => {

        beforeEach(() => {
            document.body.innerHTML = `
              <input type="text" name="name" id="name" value=""/>
              <input type="radio" name="age" id="age_1" value="1"/>男性
              <input type="radio" name="age" id="age_2" value="2"/>女性
              <input type="checkbox" name="enquete" id="enquete_1" value="1"/>回答1
              <input type="checkbox" name="enquete" id="enquete_2" value="2"/>回答2
              <select id="job">
                <option value="1">会社員</option>
                <option value="2">学生</option>
                <option value="3">その他</option>
              </select>
            `;
        });

        const idList = [
            "#name",
            "input:radio[name='age']",
            "input:checkbox[name='enquete']",
            "#job"
        ];

        // ここにテストを書く

    });

});

focus イベントに関するメソッドのテストを書く

setFocused メソッド

以下のテストを書いて、

        test("focusイベントが発生するとfocusedプロパティにselectorがセットされる", () => {
            let form = new Form(idList);
            expect(form.focused).not.toHaveProperty("#name", true);
            expect(form.focused).not.toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).not.toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).not.toHaveProperty("#job", true);

            $("#name").focus();
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).not.toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).not.toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).not.toHaveProperty("#job", true);

            $("input:radio[name='age']").focus();
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).not.toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).not.toHaveProperty("#job", true);

            $("input:checkbox[name='enquete']").focus();
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).not.toHaveProperty("#job", true);

            $("#job").focus();
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).toHaveProperty("#job", true);
        });

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

f:id:ksby:20171213011149p:plain

isAllFocused メソッド

以下のテストを書いて、

        test("isAllFocused メソッドのテスト", () => {
            const idList2 = [
                "input:radio[name='age']",
                "input:checkbox[name='enquete']"
            ];
            let form = new Form(idList);
            expect(form.isAllFocused(form, idList)).toBe(false);

            $("#name").focus();
            expect(form.isAllFocused(form, idList)).toBe(false);

            $("input:radio[name='age']").focus();
            expect(form.isAllFocused(form, idList)).toBe(false);
            expect(form.isAllFocused(form, idList2)).toBe(false);

            $("input:checkbox[name='enquete']").focus();
            expect(form.isAllFocused(form, idList)).toBe(false);
            expect(form.isAllFocused(form, idList2)).toBe(true);

            $("#job").focus();
            expect(form.isAllFocused(form, idList)).toBe(true);
        });

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

f:id:ksby:20171213064213p:plain

setFocusedFromList メソッド

以下のテストを書いて、

        test("setFocusedFromList メソッドのテスト", () => {
            let form = new Form(idList);
            expect(form.isAllFocused(form, idList)).toBe(false);
            form.setFocusedFromList(form, idList);
            expect(form.isAllFocused(form, idList)).toBe(true);

            const idList2 = [
                "input:radio[name='age']",
                "input:checkbox[name='enquete']"
            ];
            let form2 = new Form(idList);
            expect(form2.isAllFocused(form2, idList)).toBe(false);
            form2.setFocusedFromList(form2, idList2);
            expect(form2.isAllFocused(form2, idList)).toBe(false);
            expect(form2.isAllFocused(form2, idList2)).toBe(true);
        });

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

f:id:ksby:20171213065154p:plain

forceAllFocused メソッド

以下のテストを書いて、

        test("forceAllFocused メソッドのテスト", () => {
            let form = new Form(idList);
            expect(form.isAllFocused(form, idList)).toBe(false);
            form.forceAllFocused(form);
            expect(form.isAllFocused(form, idList)).toBe(true);
        });

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

f:id:ksby:20171213065840p:plain

backupFocusedState, restoreFocusedState メソッド

以下のテストを書いて、

        test("backupFocusedState, restoreFocusedState メソッドのテスト", () => {
            let form = new Form(idList);
            form.forceAllFocused(form);
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).toHaveProperty("#job", true);
            expect(form.backupFocused).not.toHaveProperty("#name", true);
            expect(form.backupFocused).not.toHaveProperty("input:radio[name='age']", true);
            expect(form.backupFocused).not.toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.backupFocused).not.toHaveProperty("#job", true);

            form.backupFocusedState(form);
            expect(form.backupFocused).toHaveProperty("#name", true);
            expect(form.backupFocused).toHaveProperty("input:radio[name='age']", true);
            expect(form.backupFocused).toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.backupFocused).toHaveProperty("#job", true);

            form.focused = {};
            form.restoreFocusedState(form);
            expect(form.focused).toHaveProperty("#name", true);
            expect(form.focused).toHaveProperty("input:radio[name='age']", true);
            expect(form.focused).toHaveProperty("input:checkbox[name='enquete']", true);
            expect(form.focused).toHaveProperty("#job", true);
        });

実行するとテストが失敗しました。。。

f:id:ksby:20171213072333p:plain

いろいろ調べて How to duplicate object properties in another object?Object.keys() の記事を見つけました。オブジェクトのプロパティをコピーするのに配列のコピーと勘違いしていたからでした。。。 Form.js を以下のように変更します。

function Form(idList) {
    this.idList = idList;
    this.focused = {};
    this.backupFocused = {};
    addFocusEventListener(this);
}

..........

/**
 * form.focused の値をバックアップする
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.backupFocusedState = function (form) {
    Object.keys(form.focused).forEach(function (key) {
        form.backupFocused[key] = form.focused[key];
    });
};

/**
 * form.focused の値を form.backupFocusedState を呼び出した時の状態に戻す
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.restoreFocusedState = function (form) {
    Object.keys(form.backupFocused).forEach(function (key) {
        form.focused[key] = form.backupFocused[key];
    });
};
  • focused, backupFocused のプロパティの初期化を []{} に変更します。
  • backupFocusedState, restoreFocusedState メソッド内の処理を Object.keys() を使用する方法に変更します。

再度テストを実行すると今度は成功しました。

f:id:ksby:20171213231918p:plain

これまで作成したテストを全て実行しても成功しました。

f:id:ksby:20171213232032p:plain

値の入力有無をチェックするメソッドのテストを書く

isAllEmpty メソッド

以下のテストを書いて、

    describe("値の入力有無をチェックするメソッドのテストを書く", () => {

        beforeEach(() => {
            document.body.innerHTML = `
              <input type="text" name="name" id="name" value=""/>
              <input type="radio" name="age" id="age_1" value="1"/>男性
              <input type="radio" name="age" id="age_2" value="2"/>女性
              <input type="checkbox" name="enquete" id="enquete_1" value="1"/>回答1
              <input type="checkbox" name="enquete" id="enquete_2" value="2"/>回答2
              <select id="job">
                <option value="">選択してください</option>
                <option value="1">会社員</option>
                <option value="2">学生</option>
                <option value="3">その他</option>
              </select>
            `;
        });

        const idList = [
            "#name",
            "input:radio[name='age']",
            "input:checkbox[name='enquete']",
            "#job"
        ];

        test("isAllEmpty メソッドのテスト", () => {
            let form = new Form(idList);
            expect(form.isAllEmpty(idList)).toBe(true);

            // input[type='text']
            $("#name").val("a");
            expect(form.isAllEmpty(idList)).toBe(false);
            $("#name").val("");
            expect(form.isAllEmpty(idList)).toBe(true);

            // input[type='radio']
            $("input:radio[name='age'][value='1']").prop("checked", true);
            expect(form.isAllEmpty(idList)).toBe(false);
            $("input:radio[name='age'][value='1']").prop("checked", false);
            expect(form.isAllEmpty(idList)).toBe(true);
            $("input:radio[name='age'][value='2']").prop("checked", true);
            expect(form.isAllEmpty(idList)).toBe(false);
            $("input:radio[name='age'][value='2']").prop("checked", false);
            expect(form.isAllEmpty(idList)).toBe(true);

            // input[type='checkbox']
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            expect(form.isAllEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            expect(form.isAllEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", true);
            expect(form.isAllEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", false);
            expect(form.isAllEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", true);
            expect(form.isAllEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", false);
            expect(form.isAllEmpty(idList)).toBe(true);

            // select
            $("#job").val("1");
            expect(form.isAllEmpty(idList)).toBe(false);
            $("#job").val("2");
            expect(form.isAllEmpty(idList)).toBe(false);
            $("#job").val("3");
            expect(form.isAllEmpty(idList)).toBe(false);
            $("#job").val("");
            expect(form.isAllEmpty(idList)).toBe(true);
        });

    });

実行するとテストが失敗しました。最初の expect(form.isAllEmpty(idList)).toBe(true); で失敗していますが、checkbox の処理を実装していないので、当然ですね。。。

f:id:ksby:20171215003536p:plain

Form.js を以下のように変更します。

/**
 * idList で渡された id の要素が全て未入力/未選択かチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:全て未入力/未選択である, false:1つ以上入力/選択されているものがある
 */
Form.prototype.isAllEmpty = function (idList) {
    var allEmpty = true;
    idList.forEach(function (id) {
        if ($(id).attr("type") === "radio") {
            if ($(id + ":checked").val() !== undefined) {
                allEmpty = false;
            }
        } else if ($(id).attr("type") === "checkbox") {
            if ($(id + ":checked").length > 0) {
                allEmpty = false;
            }
        } else if ($(id).val() !== "") {
            allEmpty = false;
        }
    });
    return allEmpty;
};
  • else if ($(id).attr("type") === "checkbox") { ... } の処理を追加します。

再度テストを実行すると今度は成功しました。

f:id:ksby:20171215011024p:plain

isAnyEmpty メソッド

isAnyEmpty メソッドも checkbox の実装をしていないので、テストの前に修正します。Form.js を以下のように変更します。

/**
 * idList で渡された id の要素の中に未入力/未選択のものがあるかチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:未入力/未選択のものがある, false:全て入力/選択されている
 */
Form.prototype.isAnyEmpty = function (idList) {
    var anyEmpty = false;
    idList.forEach(function (id) {
        if ($(id).attr("type") === "radio") {
            if ($(id + ":checked").val() === undefined) {
                anyEmpty = true;
            }
        } else if ($(id).attr("type") === "checkbox") {
            if ($(id + ":checked").length === 0) {
                anyEmpty = true;
            }
        } else if ($(id).val() === "") {
            anyEmpty = true;
        }
    });
    return anyEmpty;
};
  • else if ($(id).attr("type") === "checkbox") { ... } を追加します。

以下のテストを書いて、

        test("isAnyEmpty メソッドのテスト", () => {
            let form = new Form(idList);
            expect(form.isAnyEmpty(idList)).toBe(true);

            $("#name").val("a");
            $("input:radio[name='age'][value='1']").prop("checked", true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            $("#job").val("1");
            expect(form.isAnyEmpty(idList)).toBe(false);

            $("#name").val("");
            expect(form.isAnyEmpty(idList)).toBe(true);
            $("#name").val("a");
            expect(form.isAnyEmpty(idList)).toBe(false);

            $("input:radio[name='age'][value='1']").prop("checked", false);
            expect(form.isAnyEmpty(idList)).toBe(true);
            $("input:radio[name='age'][value='1']").prop("checked", true);
            expect(form.isAnyEmpty(idList)).toBe(false);

            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            expect(form.isAnyEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            expect(form.isAnyEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", true);
            expect(form.isAnyEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            expect(form.isAnyEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", false);
            expect(form.isAnyEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);

            $("#job").val("");
            expect(form.isAnyEmpty(idList)).toBe(true);
            $("#job").val("1");
            expect(form.isAnyEmpty(idList)).toBe(false);
        });

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

f:id:ksby:20171216071456p:plain

isAnyNotEmpty メソッド

isAnyNotEmpty メソッドも checkbox の実装をしていないので、テストの前に修正します。Form.js を以下のように変更します。

/**
 * idList で渡された id の要素の中に1つでも入力/選択されているものがあるかチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:1つでも入力/選択されている, false:全て未入力/未選択である
 */
Form.prototype.isAnyNotEmpty = function (idList) {
    var anyNotEmpty = false;
    idList.forEach(function (id) {
        if ($(id).attr("type") === "radio") {
            if ($(id + ":checked").val() !== undefined) {
                console.log(id + ", " + $(id + ":checked").val());
                anyNotEmpty = true;
            }
        } else if ($(id).attr("type") === "checkbox") {
            if ($(id + ":checked").length > 0) {
                console.log(id + ", " + $(id + ":checked").length);
                anyNotEmpty = true;
            }
        } else if ($(id).val() !== "") {
            console.log(id + ", " + $(id).val());
            anyNotEmpty = true;
        }
    });
    return anyNotEmpty;
};
  • else if ($(id).attr("type") === "radio") { ... } を追加します。

以下のテストを書いて、

        test("isAnyNotEmpty メソッドのテスト", () => {
            let form = new Form(idList);
            expect(form.isAnyNotEmpty(idList)).toBe(false);

            // input[type='text']
            $("#name").val("a");
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("#name").val("");
            expect(form.isAnyNotEmpty(idList)).toBe(false);

            $("input:radio[name='age'][value='1']").prop("checked", true);
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("input:radio[name='age'][value='1']").prop("checked", false);
            expect(form.isAnyNotEmpty(idList)).toBe(false);

            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            expect(form.isAnyNotEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", true);
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", false);
            expect(form.isAnyNotEmpty(idList)).toBe(false);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", true);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", true);
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("input:checkbox[name='enquete'][value='1']").prop("checked", false);
            $("input:checkbox[name='enquete'][value='2']").prop("checked", false);
            expect(form.isAnyNotEmpty(idList)).toBe(false);

            $("#job").val("1");
            expect(form.isAnyNotEmpty(idList)).toBe(true);
            $("#job").val("");
            expect(form.isAnyNotEmpty(idList)).toBe(false);
        });

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

f:id:ksby:20171216184029p:plain

ここまで作成したテストが全て成功することを確認します。

f:id:ksby:20171216185101p:plain

続きます。。。

Jest + IntelliJ IDEA を組み合わると jQuery を利用した Javascript のモジュールのテストが書きやすいです。document.body.innerHTML に必要最低限の HTML だけ記述して動作確認が出来るのが便利ですね。

IntelliJ IDEA を 2017.3.1 へバージョンアップしてから、Form.js のテストを続けます。

履歴

2017/12/16
初版発行。