Sequelize + Jest のユニットテスト環境構築手順

# はじめに
Sequelize な環境で開発を進めていくに当たり、
環境構築に手間取らず async/await なテストしたかったときに、
Jest を採用したらスグにテストを書き始められて良かったので記事にしました。

プロジェクトファイルは Github にアップしておきました↓
https://github.com/nuhs/sequelize-jest

# Sequelize のセットアップ
まずは npm で Sequelize のインストールを行います。
今回は Sequelize の裏側に postgresql を使用するので、
ついでに pg ライブラリもインストールします。

```shell
⊨ npm install --save sequelize pg
⊨ npm install --save-dev sequelize-cli
```

インストール完了後、Sequelize の初期化コマンドを実行します。

```shell
⊨ npx sequelize init

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Created "config/config.json"
Successfully created models folder at "/Users/nika/Desktop/sequelize-jest/models".
Successfully created migrations folder at "/Users/nika/Desktop/sequelize-jest/migrations".
Successfully created seeders folder at "/Users/nika/Desktop/sequelize-jest/seeders".
```

初期化コマンドの実行に成功すると、Sequelize を使用するのに必要なファイル群がプロジェクトフォルダ内に生成されます。 
プロジェクトルートに config/config.json が生成されているはずなので PostgreSQL と接続出来るように書き換えます。

```json:config/config.json
{
  "development": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_development",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_test",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_production",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}
```

上記で設定した config/config.json そのままで動作確認するために、
テスト用に PostgreSQL ユーザを作成しました。
本記事のサンプルを利用するためだけの用途でご利用ください。

```
⊨ psql postgres
psql (11.2)
Type "help" for help.

postgres=# CREATE ROLE sequelize_jest with login password 'password';
CREATE ROLE
postgres=# alter role sequelize_jest CREATEDB;
ALTER ROLE
postgres=# \q
```

各種データベース周りの設定が完了次第、データベースの作成を行います。

```
⊨ npx sequelize db:create

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
Database sequelize_jest_development created.
```

データベースの作成に成功できれば、Sequelize のセットアップは完了です。

# Sequelize でモデルを追加する
今回は Food というモデルを追加します。

```
⊨ npx sequelize model:create --name Food --attributes 'name:string, type:string'

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

New model was created at /Users/nika/Desktop/sequelize-jest/models/food.js .
New migration was created at /Users/nika/Desktop/sequelize-jest/migrations/20190522034233-Food.js .
```

実行に成功すると migrations というフォルダに PostgreSQL に Food モデル（テーブル）を追加するのに必要なファイルが生成されます。

早速 Sequelize 経由で PostgresSQL に Food モデル（テーブル）を追加します。

```
⊨ npx sequelize db:migrate

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
== 20190522034233-create-food: migrating =======
== 20190522034233-create-food: migrated (0.016s)
```

これで Sequelize を動作させるための最低限のセットアップが完了しました。
試しに Sequelize の動作確認してみます。

プロジェクトのルートに index.js を追加します。
index.js の中身は Food のデータを全て取得して標準出力するだけの処理です。

```javascript:index.js
const { Food } = require('./models');

const main = async () => {
    const foods = await Food.findAll({raw: true});
    console.log(foods);
}

main();
```

index.js を追加したら早速実行してみます。

```
⊨ node index.js
Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food";
[]
```

現在は Food テーブルに何もデータが入っていないため、空の配列が出力されていますが、
正しく SQL が実行されていることが標準出力から確認出来ます。

# Sequelize で初期データを用意する
空の配列だと正しくスクリプトが実行出来ているか分かりづらいため、
Food モデルのシードデータをいくつか用意します。

シードデータを追加するには seeders フォルダの中に追加する必要があります。
試しに seeders フォルダに food.js を追加して Food のデータをいくつか追加してみます。

```javascript:seeders/food.js
module.exports = {
    up: async (queryInterface, Sequelize) => {
      const data = [{
        name: 'focaccia',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'french bread',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'muffin',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'candy',
        type: 'sweets',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'chocolate',
        type: 'sweets',
        updatedAt: new Date(),
        createdAt: new Date()
      }];
      return await queryInterface.bulkInsert('Food', data);
    },
    down: async (queryInterface, Sequelize) => {
      await queryInterface.bulkDelete('Food', null, {});
    },
  };
```

上記を追加した状態でコマンドを実行します。

```
⊨ npx sequelize db:seed:all

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
== food: migrating =======
== food: migrated (0.008s)
```

コマンドの実行に成功したら、index.js を実行してみて、出力について確認してみます。

```
⊨ node index.js
Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food";
[ { id: 2,
    name: 'focaccia',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 3,
    name: 'french bread',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 4,
    name: 'muffin',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 5,
    name: 'candy',
    type: 'sweets',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 6,
    name: 'chocolate',
    type: 'sweets',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z } ]
```

無事にシードデータの追加に成功しました。

# Jest のセットアップ
基本的には公式ページの手順に沿ってインストールしていきます。
https://github.com/facebook/jest

まずは jest をインストールします。

```
npm install --save-dev jest
```

jest を npm 経由で実行出来るよう package.json の scripts の test を設定します。

```json:package.json
{
  "scripts": {
    "test": "jest"
  }
}
```

jest は実行できるようになったので、実際にテストファイルを追加します。
jest はテスト実行時に自動で `__tests__` フォルダを見に行くため、
`__tests__` フォルダの中にテストファイルを追加します。

まずは `__tests__` フォルダをプロジェクトルートに追加します。
その後 sample.js を `__tests__` フォルダに追加します。
sample.js は 1 + 2 は 3 かどうかをテストで確認しています。

```javascript:__tests__/sample.js
test('adds 1 + 2 to equal 3', () => {
    const sum = 1 + 2;
    expect(sum).toBe(3);
});
```

実際に jest でテストを実行してみます。

```
⊨ npm run test

> sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest
> jest

 PASS  __tests__/sample.js
  ✓ adds 1 + 2 to equal 3 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.878s
Ran all test suites.
```

テストの実行に成功しました。

# Sequelize のユニットテストを Jest で行うための準備

現状は開発環境下でテストを行っているため、
Sequelize でモデルを追加/更新を行うテストを行うたびに、
開発環境に影響が出てしまう状態となっています。

そのため、テストの実行が開発環境に影響を及ぼさないよう、
テスト環境下でテストをが実行できるように package.json を少し変更します。

```json:package.json
{
  "scripts": {
    "test": "export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop",
    "db:setup": "sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all",
    "db:drop": "sequelize db:drop"
  }
}
```

`__tests__` フォルダに Food モデルのテストを書くためのファイル food.js を追加します。
food.js は test 実行時に毎回シードデータが入るようになったため、その数が 5 であるかをテストしています。

```javascript:__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    test('Total of Food is 5', async () => {
        const total = await Food.count();
        expect(total).toBe(5);
    });
});
```

テストを実行します。

```
⊨ npm run test

> sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest
> export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop


> sequelize-jest@1.0.0 db:setup /Users/nika/Desktop/sequelize-jest
> sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
Database sequelize_jest_test created.

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
== 20190522034233-create-food: migrating =======
== 20190522034233-create-food: migrated (0.016s)


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
== food: migrating =======
== food: migrated (0.009s)

 PASS  __tests__/food.js
  ● Console

    console.log node_modules/sequelize/lib/sequelize.js:1176
      Executing (default): SELECT count(*) AS "count" FROM "Food" AS "Food";

 PASS  __tests__/sample.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.994s, estimated 1s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

> sequelize-jest@1.0.0 db:drop /Users/nika/Desktop/sequelize-jest
> sequelize db:drop


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
Database sequelize_jest_test dropped.
```

出力が少し長くて見づらいですが、
テスト環境下でデータベースのセットアップからシードデータの追加、jest でのテスト実行、テスト実行後のデータベース削除が実行されているのが分かります。

テスト環境を用意する前の開発環境下でテストを実行してしまうと、
都度シードデータが開発環境の Food テーブルに追加されてしまい、
それらはテスト実行ごとにクリーンアップされてくれないため、
`__tests__/food.js` のテストは 2回目以降失敗するようになります。

# Sequelize のユニットテストを Jest で書く

ここまで来ればあとは気軽にテストコードを追加していきます。

まずは models/food.js を少し改修します。
Food モデルの追加の際は type に bread か sweets しか設定出来ないようにしました。

```javascript:models/food.js
'use strict';
module.exports = (sequelize, DataTypes) => {
  const Food = sequelize.define('Food', {
    name: DataTypes.STRING,
    type: {
      type : DataTypes.STRING,
      validate: {
        isValidType: function(value, next) {
          if(['bread', 'sweets'].includes(value)) {
            return next()
          }
          next('Unacceptable type of food.')
        }
      }
    }
  }, {});
  Food.associate = function(models) {
    // associations can be defined here
  };
  return Food;
};
```

Food モデルの type に bread か sweets を設定出来ないよう改修したので、その検証のためのテストコードを `__tests__/food.js` に追加してみます。

```javascript:__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    //...
    describe('create', () => {
        describe('succeeded', () => {
            test('type of bread', async () => {
                const food = await Food.create({
                    name: 'jam bread',
                    type: 'bread',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('jam bread');
                expect(food.type).toBe('bread');
            });

            test('type of sweets', async () => {
                const food = await Food.create({
                    name: 'strawberry cake',
                    type: 'sweets',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('strawberry cake');
                expect(food.type).toBe('sweets');
            });
        });

        describe('failed', () => {
            test('type of chair', async () => {
                await expect(Food.create({
                    name: 'reclining chair',
                    type: 'chair',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true })).
                rejects.toThrow();
            });
        });
    });
});
```

テストを実行します。

```
⊨ npm run test
# jest のテスト結果の部分のみ出力
Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.123s
Ran all test suites.
```

全てのテストが無事に実行されました。試しにテストをわざと失敗させてみます。
`__tests__/food.js` に失敗するテストを追加します。

```javascript:__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    //...
        describe('failed', () => {
            //...
            test('type of chair(failed)', async () => {
                const food = await Food.create({
                    name: 'reclining chair',
                    type: 'chair',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('reclining chair');
                expect(food.type).toBe('chair');
            });
        });
    //...
```

テストを再度実行します。

```
⊨ npm run test
# jest のテスト結果の部分のみ出力
 FAIL  __tests__/food.js
  ● Food Model › create › failed › type of chair(failed)

    SequelizeValidationError: Validation error: Unacceptable type of food.

      at Promise.all.then (node_modules/sequelize/lib/instance-validator.js:74:15)

 PASS  __tests__/sample.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 5 passed, 6 total
Snapshots:   0 total
Time:        1.148s
Ran all test suites.
```

テストが失敗しました。

# おわりに

Jest を使用することで楽にテスト環境をセットアップすることが出来ました。
豊富にマッチャーも用意されているので様々なパターンのテストが Jest のみで完結できそうな印象でした。

参考リンク
https://qiita.com/ckoshien/items/9afc60546ba1c9ce04f4
https://blog.honjala.net/entry/2018/08/08/022027
https://jestjs.io/ja/