Web3.0検証(21)-MeteorでTODO管理アプリの開発(リグレッションテスト)

##### [前回] [Web3.0検証(20)-MeteorでTODO管理アプリの開発(publish/subscribeによるデータ制御)](https://qiita.com/mingchun_zhao/items/730c74eb26d387697620)

## はじめに

今回は、アプリのリグレッションテスト機能です。

## リグレッションテストの機能追加

- テスト機能を追加し、リグレッション発生せず、期待どおり機能することを確認
- リグレッションテスト(Regression Test)とは
    - プログラムの変更に伴い、システムに予想外の影響が現れていないかを確認するテスト
    - リグレッションは、`回帰テスト`、`退行テスト`とも呼ばれる
- メソッドの1つを実行し、正しく機能するか確認するテストを作成

### アプリをテストモードで実行

- 依存関係をインストール
    - `Mocha`JavaScriptテストフレームワークのテストドライバーを追加
    ```
        $ meteor add meteortesting:mocha
    ```
    - テストアサーションライブラリをインストール
    ```
        $ meteor npm install --save-dev chai
    ```

- アプリを`テストモード`で実行
    - テストドライバーパッケージを指定し、`meteor test`を実行
    - ポート番号が衝突しないように
        - 通常アプリの実行を停止するか
        - `–port XYZ`で代替ポートを指定
```
$ cd simple-todos-react
$ TEST_WATCH=1 meteor test --driver-package meteortesting:mocha
```
```
[[[[[ Tests ]]]]]

=> Started proxy.
=> Started HMR server.
=> Started MongoDB.
I20220603-21:31:00.827(9)?
I20220603-21:31:00.840(9)? --------------------------------
I20220603-21:31:00.841(9)? ----- RUNNING SERVER TESTS -----
I20220603-21:31:00.841(9)? --------------------------------
I20220603-21:31:00.841(9)?
I20220603-21:31:00.842(9)?
I20220603-21:31:00.842(9)?
I20220603-21:31:00.842(9)?   simple-todos-react
=> Started your app.

=> App running at: http://localhost:3000/
I20220603-21:31:00.845(9)?     ✓ package.json has correct name
I20220603-21:31:00.845(9)?     ✓ server is not client
I20220603-21:31:00.846(9)?
I20220603-21:31:00.846(9)?
I20220603-21:31:00.846(9)?   2 passing (8ms)
I20220603-21:31:00.847(9)?
I20220603-21:31:00.847(9)? Load the app in a browser to run client tests, or set the TEST_BROWSER_DRIVER environment variable. See https://github.com/meteortesting/meteor-mocha/blob/master/README.md#run-app-tests
```

サーバーテストは成功しました。

- ブラウザでクライアントテスト

`http://localhost:3000/`にアクセス。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/d25fcae9-6664-7f6c-5494-e7d0579fa2a4.png)

クライアントテストも成功しました。

- 上述テストが可能な理由
    - Meteorアプリケーションには、サンプルテストを含む`tests/main.js`モジュールが含まれている
    - `Mocha`などのテストフレームワークでサポートする`describe`、`it`、`assert`スタイルを使用できる

### スキャフォールド(Scaffold)テスト

- テストを複数モジュールに分割可能
- 以下の新しいテストモジュールを追加

```imports/api/tasksMethods.tests.js
import { Meteor } from 'meteor/meteor';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      it('can delete owned task', () => {});
    });
  });
}
```

- `tests/main.js`にインポート
- これ以外のテストは不要で残りコードをすべて削除
```tests/main.js
import '/imports/api/tasksMethods.tests.js';
```

### データベースをテスト

- データベースが期待どおりの状態にあることを確認
    - `Mocha`の`beforeEach`コンストラクトを使用
- テスト実行ごとに異なるランダムな`userId`に関連付けられた、単一タスクを作成

```imports/api/tasksMethods.tests.js
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { TasksCollection } from '/imports/db/TasksCollection';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });
    });
  });
}
```

### タスク削除をテスト

- 認証されたユーザーで`tasks.remove`メソッドを呼び出すテストを記述
    - タスクが削除されたことを確認
- メソッドを簡単に呼び出すため、ユーティリティパッケージをインストール
```
$ meteor add quave:testing
```

- アサーションツール`chai`から、`assert`をインポート
```imports/api/tasks.tests.js
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { mockMethodCall } from 'meteor/quave:testing';
import { assert } from 'chai';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });

      it('can delete owned task', () => {
        mockMethodCall('tasks.remove', taskId, { context: { userId } });

        assert.equal(TasksCollection.find().count(), 0);
      });
    });
  });
}
```

### その他テスト

- 必要なテストを追加

```imports/api/tasks.tests.js
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { mockMethodCall } from 'meteor/quave:testing';
import { assert } from 'chai';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });

      it('can delete owned task', () => {
        mockMethodCall('tasks.remove', taskId, { context: { userId } });

        assert.equal(TasksCollection.find().count(), 0);
      });

      it(`can't delete task without an user authenticated`, () => {
        const fn = () => mockMethodCall('tasks.remove', taskId);
        assert.throw(fn, /Not authorized/);
        assert.equal(TasksCollection.find().count(), 1);
      });

      it(`can't delete task from another owner`, () => {
        const fn = () =>
          mockMethodCall('tasks.remove', taskId, {
            context: { userId: 'somebody-else-id' },
          });
        assert.throw(fn, /Access denied/);
        assert.equal(TasksCollection.find().count(), 1);
      });

      it('can change the status of a task', () => {
        const originalTask = TasksCollection.findOne(taskId);
        mockMethodCall('tasks.setIsChecked', taskId, !originalTask.isChecked, {
          context: { userId },
        });

        const updatedTask = TasksCollection.findOne(taskId);
        assert.notEqual(updatedTask.isChecked, originalTask.isChecked);
      });

      it('can insert new tasks', () => {
        const text = 'New Task';
        mockMethodCall('tasks.insert', text, {
          context: { userId },
        });

        const tasks = TasksCollection.find({}).fetch();
        assert.equal(tasks.length, 2);
        assert.isTrue(tasks.some(task => task.text === text));
      });
    });
  });
}
```

- `package.json`ファイルの`scripts`セクションに省略形を追加
    - テストコマンドを簡単に入力できるようになる
    - Meteorアプリに付属された、事前構成済みnpmスクリプトを使用/変更

- サーバー側のテストのみ実行したい
    - 標準の`meteor npm test`コマンドを用いる、次のコマンドと等価
    - `$ meteor test --once --driver-package meteortesting:mocha`
    - すべてのテストに合格したら`0`で終了
    - `Travis CI`や`CircleCI`など、CI(継続的インテグレーション)環境での実行に適している

- アプリケーションの開発中にテストを実行したい
- または、開発サーバーが再起動するたびにテストを再実行したい
    - `meteor npm run test-app`を使用、次のコマンドと等価
    - `TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha`
        - `--full-app`を追加することで、アプリケーションコードも読み込まれる

- ブラウザからアプリを確認
    - ログイン
        - ユーザー名: meteorite
        - パスワード: password

クライアントテストとサーバーテストの両方を実行しながら、
ブラウザーでアプリを操作できるようになりました。

## おわりに

アプリのリグレッションテスト機能を実装しました。
次回も続きます。お楽しみに。

##### [次回] [Web3.0検証(22(最終回))-MeteorでTODO管理アプリの開発(デプロイ&公開)](https://qiita.com/mingchun_zhao/items/ed79a4eb5653325d890b)