かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( Jest + axios + Nock, xhr-mock でテストを書いてみる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 状態管理をするための MobX というモジュールがあり、これを使えば jQuery でも状態管理ができるらしいので Jest でテストを書いていろいろ試してみます。

参照したサイト・書籍

  1. Reactでも使える!シンプルなJavaScriptステート管理ライブラリー Mobxを試す
    https://www.webprofessional.jp/manage-javascript-application-state-mobx/

  2. MobX
    https://mobx.js.org/index.html

  3. mobx - npm
    https://www.npmjs.com/package/mobx

  4. MobX shopping cart demo
    https://jsfiddle.net/mweststrate/vxn7qgdw/

  5. jQueryプラグインを自作するには?($.fn)
    http://www.buildinsider.net/web/jqueryref/031

  6. Mobx vs Reactive Stream Libraries (RxJS, Bacon, etc)
    https://github.com/mobxjs/mobx/wiki/Mobx-vs-Reactive-Stream-Libraries-(RxJS,-Bacon,-etc)

  7. これからMobXをはじめる人へ
    http://lealog.hateblo.jp/entry/2016/12/15/113825

  8. MobX 3.0.0 の変更点について
    http://lealog.hateblo.jp/entry/2017/01/11/140607

  9. 0からはじめる MobX Part.1
    http://lealog.hateblo.jp/entry/2016/09/07/110823

  10. 0からはじめる MobX Part.2
    http://lealog.hateblo.jp/entry/2016/09/18/125202

  11. 0からはじめる MobX Part.3
    http://lealog.hateblo.jp/entry/2016/09/27/185127

  12. 0からはじめる MobX Part.4
    http://lealog.hateblo.jp/entry/2016/11/21/104613

  13. mobxjs / mobx-utils
    https://github.com/mobxjs/mobx-utils

目次

  1. MobX とは?
  2. MobX をインストールする
  3. extendObservable でプロパティを1つ持つクラスを定義し、動作を確認する
  4. extendObservable でプロパティを2つ持つクラスを定義し、動作を確認する
  5. autorun から computed で定義したプロパティを参照している場合の動作を確認する
  6. autorun からメソッドを参照している場合の動作を確認する
  7. autorun はプロパティの値を変更した時だけでなく、最初の定義時にも関数を実行する
  8. when は条件を満たした時に1度だけ動作する
  9. autorunAsync はプロパティが更新された時に動作する(定義時は動作しない)

手順

MobX とは?

試してみた感想としては、

  • オブジェクト(observable なオブジェクトとしてあらかじめ定義しておく)のプロパティの値を変更すると、そのプロパティを参照する関数が自動で実行される仕組みを記述するためのライブラリ。
  • 関数を実行するか否かを判断するための監視対象のプロパティを MobX が自動で判別してくれる。おそらくこれがかなり便利。

という感じです。あと React とか全然やったことがないので状態管理が何をするものなのかが分からなくて、書き方と動作を理解するのに結構時間を取られました。。。 また React と組み合わせるサンプルは見かけますが、Vue.js と組み合わせることはないようです(Vue.js は Vuex と組み合わせればいいので不要ということでしょうか?)。

MobX をインストールする

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

f:id:ksby:20180103191740p:plain

extendObservable でプロパティを1つ持つクラスを定義し、動作を確認する

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

"use strict";

global.$ = require("jquery");
const mobx = require("mobx");

describe("MobX の動作確認", () => {

    test("extendObservable でプロパティを1つ持つクラスを定義し、動作を確認する", () => {
        document.body.innerHTML = `
            <div id="value"></div>
        `;

        // mobx.extendObservable メソッドを使用して、Sample クラスを Observable として定義する
        //
        // function Sample() {...} で定義するなら以下のように記述する
        // function Sample() {
        //     mobx.extendObservable(this, {
        //         value: ""
        //     });
        // }
        //
        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: ""
                });
            }
        }

        // sample.value が変更されたら $("#value").text() を更新するよう定義する
        const sample = new Sample();
        mobx.autorun(() => {
            $("#value").text(sample.value);
        });

        // sample.value の値を変更すると、
        // mobx.autorun で定義した関数が実行されて、
        // $("#value").text() に sample.value の値がセットされる
        expect($("#value").text()).toBe("");
        sample.value = "test";
        expect($("#value").text()).toBe("test");
    });

});

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

f:id:ksby:20180103193651p:plain

以下のような書き方・動作になります。

  1. 変更を検知して何かさせる元となるオブジェクトを mobx.observable(...)mobx.extendObservable(...) を使用して定義する。
  2. オブジェクトが変更されたら実行する関数を mobx.autorun(...) で定義する。
  3. オブジェクトのプロパティを変更すると、mobx.autorun(...) で定義した関数が実行される。

extendObservable でプロパティを2つ持つクラスを定義し、動作を確認する

mobx.autorun(...) 内で参照していないプロパティを変更しても mobx.autorun(...) で定義した関数は実行されません。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("extendObservable でプロパティを2つ持つクラスを定義し、動作を確認する", () => {
        document.body.innerHTML = `
            <div id="value"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    data: ""
                });
            }
        }

        const sample = new Sample();
        // mbox.autorun 内では sample.value しか参照していない
        mobx.autorun(() => {
            console.log(`★★★ mobx.autorun: sample.value = ${sample.value}`);
            $("#value").text(sample.value);
        });

        // sample.value を変更すると mbox.autorun で定義した関数が実行されるが、
        // sample.data を変更しても実行されない
        console.log("●●● sample.value = \"PASS1\"");
        sample.value = "PASS1";
        console.log("▲▲▲ sample.data = \"1\"");
        sample.data = "1";
        console.log("●●● sample.value = \"PASS2\"");
        sample.value = "PASS2";
    });

テストを実行すると、sample.value を変更した時には mobx.autorun(...) で定義した関数が実行されますが、sample.data を変更しても実行されませんでした。MobX の方で何が変更されたら関数を実行するか自動的に判別してくれるようです。

f:id:ksby:20180103195146p:plain

autorun から computed で定義したプロパティを参照している場合の動作を確認する

mobx.autorun(...) で定義した関数から mobx.computed(...) で定義したプロパティを参照している場合、mobx.computed(...) で参照しているプロパティが変更されると mobx.autorun(...) で定義した関数が実行されます。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("autorun から computed で定義したプロパティを参照している場合の動作を確認する", () => {
        document.body.innerHTML = `
            <div id="personal">
                <input type="text" name="firstname" id="firstname" value="">
                <input type="text" name="lastname" id="lastname" value="">
                <div id="fullname"></div>
            </div>
        `;

        // mobx.computed() で定義した fullname プロパティから firstname + lastname の文字列
        // が取得できるようにする
        class Personal {
            constructor() {
                mobx.extendObservable(this, {
                    firstname: "",
                    lastname: "",
                    fullname: mobx.computed(() => `${this.firstname} ${this.lastname}`)
                });
            }
        }

        const personal = new Personal();
        // mobx.autorun では personal.fullname を参照する
        mobx.autorun(() => {
            $("#fullname").text(personal.fullname);
        });

        // blur イベント発生時に View から入力された値を personal オブジェクトにセットする
        [
            "firstname",
            "lastname"
        ].forEach(item => {
            $("#" + item).on("blur", event => {
                personal[item] = $(event.target).val();
            });
        });

        // $("#firstname").val() に "taro" と入力すると $("#fullname").text() も変更される
        $("#firstname").val("taro").blur();
        console.log("★★★ " + $("#fullname").text());
        // $("#lastname").val() に "tanaka" と入力しても $("#fullname").text() も変更される
        $("#lastname").val("tanaka").blur();
        console.log("★★★ " + $("#fullname").text());
    });

テストを実行すると、personal.firstname を変更しても personal.lastname を変更しても mobx.autorun(...) で定義した関数が実行されていることが確認できます。

f:id:ksby:20180103202540p:plain

autorun からメソッドを参照している場合の動作を確認する

mobx.computed(...) で定義したプロパティの場合 mobx.computed(...) から参照されているプロパティの値が変更されれば mobx.autorun(...) で定義した関数が実行されますが、メソッドの場合どうなるのか確認してみます。

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

    test("autorun からメソッドを参照している場合の動作を確認する", () => {
        document.body.innerHTML = `
            <div id="personal">
                <input type="text" name="firstname" id="firstname" value="">
                <input type="text" name="lastname" id="lastname" value="">
                <div id="fullname"></div>
            </div>
        `;

        // fullname メソッドから firstname + lastname の文字列
        // が取得できるようにする
        class Personal {
            constructor() {
                mobx.extendObservable(this, {
                    firstname: "",
                    lastname: "",
                    fullname() {
                        return `${this.firstname} ${this.lastname}`
                    }
                });
            }
        }

        const personal = new Personal();
        // mobx.autorun では personal.fullname() を参照する
        mobx.autorun(() => {
            $("#fullname").text(personal.fullname());
        });

        // blur イベント発生時に View から入力された値を personal オブジェクトにセットする
        [
            "firstname",
            "lastname"
        ].forEach(item => {
            $("#" + item).on("blur", event => {
                personal[item] = $(event.target).val();
            });
        });

        // $("#firstname").val() に "taro" と入力すると $("#fullname").text() も変更される
        $("#firstname").val("taro").blur();
        console.log("★★★ " + $("#fullname").text());
        // $("#lastname").val() に "tanaka" と入力しても $("#fullname").text() も変更される
        $("#lastname").val("tanaka").blur();
        console.log("★★★ " + $("#fullname").text());
    });

テストを実行すると、先程と同様に personal.firstname を変更しても personal.lastname を変更しても mobx.autorun(...) で定義した関数が実行されていることが確認できます。

f:id:ksby:20180103203530p:plain

autorun はプロパティの値を変更した時だけでなく、最初の定義時にも関数を実行する

mobx.autorun(...) で定義した関数はプロパティが変更された時だけでなく、最初の定義時にも実行されます。実行した時に監視するプロパティをチェックするようです。mobx.when(...)mobx.autorunAsync(...) は最初の定義時には実行されません。

mobx.autorunAsync(...) は定義時に実行しなくても監視するプロパティが判別できるようなので mobx.autorun(...) も定義時に実行しなくてもよい気がするのですが、そこがよく分かりませんでした。。。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("autorun はプロパティの値を変更した時だけでなく、最初の定義時にも関数を実行する", () => {
        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: ""
                });
            }
        }

        let cnt = 0;
        const sample = new Sample();
        console.log("(1) mobx.autorun は定義時に1度実行される");
        mobx.autorun(() => {
            console.log("mobx.autorun の関数が実行されました");
            $("#value").text(sample.value);
            cnt++;
        });

        console.log("(2) mobx.autorun はプロパティを変更しても実行される");
        sample.value = "1";
    });

テストを実行すると、最初の定義時にも mobx.autorun(...) に渡した関数が実行されていることが確認できます。

f:id:ksby:20180105004353p:plain

また以下のように最初の実行時に sample.value を参照しないと、

    test("autorun の初回実行時にプロパティを参照しないと、プロパティを変更しても関数は実行されない", () => {
        class Sample {
            constructor() {
                this.autorunFirstFlg = true;
                mobx.extendObservable(this, {
                    value: ""
                });
            }
        }

        let cnt = 1;
        const sample = new Sample();
        console.log("(1) mobx.autorun の最初の定義時にプロパティを参照しないようにする");
        mobx.autorun(() => {
            console.log(`${cnt}回目: mobx.autorun の関数が実行されました`);
            if (sample.autorunFirstFlg) {
                sample.autorunFirstFlg = false;
                return;
            }

            // 最初の実行時にはここを通らない
            $("#value").text(sample.value);
            cnt++;
        });

        console.log("(2) プロパティを変更しても mobx.autorun は実行されない");
        sample.value = "1";
    });

プロパティを変更しても mobx.autorun(...) に渡した関数は実行されません。

f:id:ksby:20180105011452p:plain

when は条件を満たした時に1度だけ動作する

mobx.autorun(...) で定義した関数は参照しているプロパティが変更されれば何度でも実行されますが、mobx.when(...) は条件を満たした時に1度だけ実行されます。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("when は条件を満たした時に1度だけ動作する", () => {
        document.body.innerHTML = `
            <div id="value"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    whenExecuteFlg: false
                });
            }
        }

        let cnt = 1;
        const sample = new Sample();
        console.log("mobx.when は最初の定義時には実行されない");
        mobx.when(
            // 第1引数の関数の条件を満たした時だけ、第2引数の関数が実行される
            // また第1引数の条件で参照する変数は observable でなければいけない
            () => sample.whenExecuteFlg === true,
            () => {
                console.log(`${cnt}回目: mobx.when の関数が実行されました`);
                $("#value").text(sample.value);
                cnt++;
            }
        );

        // sample.value の値を変更するたけでは $("#value").text() には反映されない
        sample.value = "test";
        expect($("#value").text()).toBe("");
        // sample.whenExecuteFlg を true にすると反映される
        sample.whenExecuteFlg = true;
        expect($("#value").text()).toBe("test");
        // sample.whenExecuteFlg を false に戻して再度 true にしても、もう mobx.when は実行されない
        sample.whenExecuteFlg = false;
        sample.value = "sample";
        sample.whenExecuteFlg = true;
        expect($("#value").text()).toBe("test");
    });

テストを実行すると、1度だけ実行されることが確認できます。

f:id:ksby:20180105014530p:plain

MobX-utils の fromPromise メソッドを使用して、非同期にデータを取得して処理するサンプルを作成してみます。

まずは npm install --save mobx-utils コマンドを実行して MobX-utils をインストールします。

f:id:ksby:20180105015129p:plain

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

"use strict";

global.$ = require("jquery");
const mobx = require("mobx");
const mobxUtils = require("mobx-utils");
const openWeatherMapHelper = require("lib/util/OpenWeatherMapHelper.js");
const xhrmock = require('xhr-mock');

describe("MobX の動作確認", () => {

    beforeEach(() => {
        xhrmock.setup();
    });

    afterEach(() => {
        xhrmock.teardown();
    });

    ..........

    test("when + mobx-utils.fromPromise のサンプル", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="weather"></div>
        `;

        // mobx-utils の fromPromise メソッドを使用して
        // openWeatherMapHelper.getCurrentWeatherDataByCityName を呼び出す
        const result = mobxUtils.fromPromise(
            openWeatherMapHelper.getCurrentWeatherDataByCityName("Tokyo"));
        // 非同期処理が完了したら ( result.state が "pending" でなくなったら )、
        // 取得した天気を $("#weather").text() にセットする
        mobx.when(
            () => result.state !== "pending",
            () => {
                const json = result.value.data;
                $("#weather").text(json.weather[0].main);
            }
        );

        // 1秒後に $("#weather").text() にモックで定義した天気("Rain")がセットされていることを確認する
        expect($("#wheather").text()).toBe("");
        setTimeout(() => {
            expect($("#weather").text()).toBe("Rain");
            done();
        }, 1000);
    });

});

テストを実行すると成功することが確認できます。

f:id:ksby:20180105021802p:plain

autorunAsync はプロパティが更新された時に動作する(定義時は動作しない)

mobx.autorunAsync(...) はプロパティが更新されると非同期で処理を実行します。mobx.autorun(...) は定義時にも実行されますが、 mobx.autorunAsync(...) は定義時には実行されません。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("autorunAsync は observable なプロパティが変更された時に動作する(定義時は動作しない)", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: ""
                });
            }
        }

        const area = new Area();
        // area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする
        mobx.autorunAsync(async () => {
            console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
            const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(area.name);
            const json = res.data;
            area.weather = json.weather[0].main;
        });
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather);
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            done();
        }, 1000);
    });

テストを実行すると、定義時には実行されず、関数内で参照しているプロパティが更新された時だけ実行されることが確認できます。

f:id:ksby:20180106070750p:plain

もう少し試したいことがあるので続きます。

履歴

2018/01/06
初版発行。