Sencha Architect と Sencha Test の開発環境に REST API モックサーバーを追加する


Sencha Architect と Sencha Test の開発環境に REST API モックサーバーを追加する手順をメモっとく。

この記事では、`Ext.ux.ajax.SimManager` と `Ext.ux.ajax.Simlet` を用いて REST API のモックサーバーを Sencha Architect と Sencha Test の開発環境に追加する。そうすることでコンポーネントごとにサーバーとの通信が発生するテストを書けるようにしている。

[Sencha Architect と Sencha Test で開発環境を構築する](https://qiita.com/takuya0301/items/ca578ae6dae9d625ee8f)の続き。

# 前提条件

この記事では下記バージョンの Sencha ツール群を使用している。

| Tool             | Version   |
|------------------|-----------|
| Sencha Architect | 4.2.7     |
| Sencha Ext JS    | 7.1.0.48  |
| Sencha Cmd       | 7.1.0.16  |
| Sencha Test      | 2.3.0.328 |

# REST API モックサーバーを追加する

app.json にある依存パッケージの設定に `"ux"` を追加する。`Ext.ux.ajax.SimManager` と `Ext.ux.ajax.Simlet` を使用するために必要な設定になる。

```shell-session
$ sed -i "" 's|"font-awesome"$|"font-awesome", "ux"|' test/app.json
```

RestSimlet.js と Server.js を app ディレクトリに追加する。

```javascript:test/app/RestSimlet.js
Ext.define('RestSimlet', {
    extend: 'Ext.ux.ajax.JsonSimlet',
    alias: 'simlet.rest',

    doGet: function (ctx) {
        ctx.matches = this.getMatches(ctx);
        return this.callParent(arguments);
    },

    doPut: function (ctx) {
        return this.doGet(ctx);
    },

    doDelete: function (ctx) {
        return this.doGet(ctx);
    },

    privates: {
        getMatches: function (ctx) {
            var url = ctx.url.split(/\?|#/)[0],
                pattern = ctx.xhr.simlet.url;
            return url.match(pattern);
        }
    }
});
```

```javascript:test/app/Server.js
Ext.define('Server', {
    requires: [
        'RestSimlet'
    ],

    singleton: true,

    start: resources => {
        Ext.ux.ajax.SimManager.init({
            defaultType: 'rest',
            delay: 0
        }).register([{
            url: RegExp('^/(\\w+)(/(\\d+))?$'),
            data: ctx => {
                var method = ctx.method.toUpperCase(),
                    resource = ctx.matches[1],
                    id = Number(ctx.matches[3]);

                resources = resources || {};
                resources[resource] = resources[resource] || [];

                var records = resources[resource];

                switch (method) {
                    case 'GET':
                        if (id) {
                            var index = records.findIndex(record => record.id === id);
                            if (index < 0) {
                                this.status = 404;
                                this.statusText = 'Not Found';
                                return;
                            } else {
                                this.status = 200;
                                this.statusText = 'OK';
                                return records[index];
                            }
                        } else {
                            this.status = 200;
                            this.statusText = 'OK';
                            return records;
                        }

                    case 'POST':
                        if (id) {
                            this.status = 405;
                            this.statusText = 'Method Not Allowed';
                            return;
                        } else {
                            var record = Ext.decode(ctx.xhr.body);
                            if (record) {
                                record.id = records.reduce((maximum, record) => Math.max(maximum, record.id), 0) + 1;
                                records.push(record);

                                this.status = 201;
                                this.statusText = 'Created';
                                return record;
                            } else {
                                this.status = 400;
                                this.statusText = 'Bad Request';
                                return;
                            }
                        }

                    case 'PUT':
                        if (id) {
                            var record = Ext.decode(ctx.xhr.body);
                            if (record) {
                                record.id = id;

                                var index = records.findIndex(record => record.id === id);
                                if (index < 0) {
                                    records.push(record);

                                    this.status = 201;
                                    this.statusText = 'Created';
                                    return record;
                                } else {
                                    records[index] = record;

                                    this.status = 200;
                                    this.statusText = 'OK';
                                    return record;
                                }
                            } else {
                                this.status = 400;
                                this.statusText = 'Bad Request';
                                return;
                            }
                        } else {
                            this.status = 405;
                            this.statusText = 'Method Not Allowed';
                            return;
                        }

                    case 'DELETE':
                        if (id) {
                            var index = records.findIndex(record => record.id === id);
                            if (index < 0) {
                                this.status = 404;
                                this.statusText = 'Not Found';
                                return;
                            } else {
                                records.splice(index, 1);

                                this.status = 204;
                                this.statusText = 'No Content';
                                return;
                            }
                        } else {
                            this.status = 405;
                            this.statusText = 'Method Not Allowed';
                            return;
                        }
                }
            }
        }]);
    },

    stop: () => Ext.ux.ajax.SimManager.simlets = []
});
```

app.js に依存クラスとして `Server` を追加する。

```javascript:test/app.js
/*
 * This call registers your application to be launched when the browser is ready.
 */
Ext.application({
    requires: [
        'Ext.Panel',
        'Server'
    ],

    name: 'ExampleTest',

    launch: () => {
        var config = { floated: true, right: 0, bottom: 0 };
        if (Ext.isChrome) Ext.merge(config, { iconCls: 'x-fab fa-chrome',  title: 'Chrome'  });
        if (Ext.isEdge  ) Ext.merge(config, { iconCls: 'x-fab fa-edge',    title: 'Edge'    });
        if (Ext.isGecko ) Ext.merge(config, { iconCls: 'x-fab fa-firefox', title: 'Firefox' });
        if (Ext.isOpera ) Ext.merge(config, { iconCls: 'x-fab fa-opera',   title: 'Opera'   });
        if (Ext.isSafari) Ext.merge(config, { iconCls: 'x-fab fa-safari',  title: 'Safari'  });
        Ext.create('Ext.Panel', config).show();
    }
});
```

```shell-session
$ git add .
$ git commit -m "Add REST API mock server"
```

# REST API モックサーバーの動作を確認する

Sencha Architect を開く。

User モデルを作成する。

* userClassName: User
* fields:
    * id
    * name
* proxy:
    * type: rest
    * url: /users

![User model](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/52db0181-0096-d5bb-29d1-19a8e684443b.png)

Users ストアを作成する。

* userClassName: Users
* storeId: Users
* autoLoad: true
* model: Example.model.User
* data:

```json
[
    { id: 1, name: "Alice"   },
    { id: 2, name: "Bob"     },
    { id: 3, name: "Charlie" },
    { id: 4, name: "Dave"    }
]
```

![Users store](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/df0262e5-7680-8c3b-67a6-67e838b23aac.png)


Users グリッドを作成する。デザインビューにレコードが表示されていれば動作している。

* userClassName: Books
* store: Users
* columns:
    * Column
        * xtype: gridcolumn
        * flex: auto
        * width: (none)
        * dataIndex: id
        * text: id
    * Column
        * xtype: gridcolumn
        * flex: 1
        * width: (none)
        * dataIndex: name
        * text: name

![Code view of Users grid](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/ae3cd606-4514-7ece-ff38-85174d5cd9ba.png)

![Design view of Users grid](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/197bf77c-768e-beae-4725-731deebbf838.png)

Sencha Architect のプロジェクトを保存する。そして、プレビューをクリックし、下記のように表示されていれば実装がきちんとできている。

![Users grid in main application](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/dd645aa2-3342-1cae-b588-968d45a6bc9d.png)

Sencha Studio で Jasmine テストスイートを UsersTest という名前で作成する。

![Create Jasmine test suite](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67627/006ae249-51ff-6267-4571-2e74430ae053.png)

`Users` グリッドに自動ロードで5件のユーザーが読み込まれることを確認するテストを記述する。

ここで `beforeEach` メソッドで、REST API モックサーバーを開始させ、テスト対象の `Users` ストアと `Users` コンポーネントを生成し、テストアプリケーションのビューポートにコンポーネントを追加している。テストではそのグリッドを取得し、グリッドが参照するストアのレコード数が5であることを検証している。afterEach メソッドでテストのために生成したストアとコンポーネントを破棄し、REST API モックサーバーを停止させ、ひとつのテストケースが完了する。

```javascript:test/test/component/UsersTest.js
describe("UsersTest", function () {
    let store;
    let component;

    beforeEach(function () {
        Server.start({
            users: [
                { id: 1, name: "Alice"   },
                { id: 2, name: "Bob"     },
                { id: 3, name: "Charlie" },
                { id: 4, name: "Dave"    },
                { id: 5, name: "Eve"     }
            ]
        });
        store = Ext.create("Example.store.Users");
        component = Ext.create("Example.view.Users");
        Ext.getApplication().viewport.add(component);
    });

    afterEach(function () {
        component.destroy();
        store.destroy();
        Server.stop();
    });

    it("loads 5 records", function () {
        ST.grid("users")
            .and(grid => {
                var users = grid.getStore();
                expect(users.count()).toBe(5);
            });
    });
});
```

このテストが無事に完了すれば開発環境に REST API モックサーバーを追加できたことになる。

---

この記事では Sencha Architect と Sencha Test の開発環境に REST API モックサーバーを追加する方法について説明した。SPA のコンポーネントテストも高速に実現できる環境なので、興味のある人はトライアル版で試してみてほしい。