💯

GitHub Actions の使い捨て環境でマルチDBの Jest を実行する

2023/08/01に公開

はじめに

複数の DB に対応したアプリケーションを作っていると、 ちょっとした修正が SQL の方言や Knex のようなクエリビルダの仕様によって、不具合を起こすことがあります。

今、個人開発しているヘッドレス CMS も MySQL / PostgreSQL / SQLite から使う DB をユーザーが選択できるため、機能開発ではロジックだけでなく、それぞれの DB で正しく CRUD できることも確認する必要があります。

これはテストも同様で、1つのケースで複数の DB に接続して確認するなど、一工夫が必要になってきます。しかし、これらを CI で自動化できれば、思わぬバグに見舞われることも軽減できます。

今回は、GitHub Actions で、使い捨ての環境を立ち上げながら、複数の DB で Jest を実行する手順を紹介します。

紹介するコードはすべて GitHub で公開していますので、適宜ご参照くださいmm

環境

内容
テストツール Jest
DB MySQL, PostgreSQL, SQLite...etc 何個でも!
CI GitHub Actions

ゴール

Don't repeat 「MySQLはOK、ポスグレはエラー😱」

テストの流れ

  • Docker を起動する
  • Jest のセットアップでマイグレーション/seed を実行する
  • 複数の DB でテスト実行!

Jest

コンフィグレーション

まずは、テストで使う DB の初期化を行います。 Jest コンフィグレーションファイルの globalSetup にセットアップコードを指定します。

jest.integrations.config.mjs
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
  ...
  globalSetup: './test/setups/setup.ts',
  ...
};

ここではテストする DB ごとにマイグレーションの実行と seed データを投入します。通常と違うのは、testDatabases という配列で、これらを繰り返し行っているところです。

setup.ts
export default async (): Promise<void> => {
  for (const testDatabase of testDatabases) {
    const database = knex(config.knexConfig[testDatabase]!);

    if (testDatabase === 'sqlite3') {
      writeFileSync('test.db', '');
    }

    await database.migrate.latest();
    await database.seed.run();
  }
};

続いてテストコードです。

テストコード

こちらも同じく testDatabases でイテレーションさせながら、後はいつものテストケースを書けばOK 🙆

describe('Users', () => {
  describe('Create', () => {
    it.each(testDatabases)('%s - should create', async (database) => {
      const connection = databases.get(database)!;

      const repository = new UsersRepository(tableName, { knex: connection });
      const result = await repository.create(user);

      expect(result).toBeTruthy();
    });
  });
});

1つ注意する点としては、afterAll ですべてのコネクションを破棄することです。そうしないと、テストが Pass しているにも関わらず Jest がプロセスを終了せずに数分間待機してしまうことになります。

const databases = new Map<string, Knex>();

beforeAll(async () => {
  for (const database of testDatabases) {
    databases.set(database, knex(config.knexConfig[database]!));
  }
});

afterAll(async () => {
  for (const [_, connection] of databases) {
    await connection.destroy();
  }
});

テスト実行

以上で設定完了したので、さっそく実行してみましょう!

yarn test:int

こんな感じでテストが成功します~~やったね! 🙌

Run yarn test:int

yarn run v1.22.19
$ node --loader ts-node/esm --experimental-vm-modules node_modules/jest/bin/jest.js --config=jest.integrations.config.mjs
(node:2144) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

Setup databases: [ 'sqlite3', 'mysql', 'maria', 'postgres' ]
🟢 Starting tests!

(node:2144) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
PASS test/api/repositories/projectSettings.test.ts (6.145 s)
PASS test/api/repositories/users.test.ts
-------------------------|---------|----------|---------|---------|------------------------------------------------------

File % Stmts % Branch % Funcs % Lines Uncovered Line #s
All files 72.97 83.33 40 72.97
src 100 100 100 100
env.ts 100 100 100 100
src/api/database 39.74 100 0 39.74
connection.ts 39.74 100 0 39.74 32-78
src/api/repositories 54.77 82.35 41.66 54.77
base.ts 87.91 70 54.54 87.91 31-35,77-78,85-86,89-90
projectSettings.ts 72.22 100 50 72.22 13-17
users.ts 29.54 100 27.27 29.54 13-17,25-30,40-49,52-63,66-81,84-107,110-116,119-131
src/exceptions 68 100 33.33 68
base.ts 66.66 100 50 66.66 12-17
invalidCredentials.ts 71.42 100 0 71.42 5-6
src/exceptions/database 100 100 100 100
recordNotUnique.ts 100 100 100 100
test 96.15 100 0 96.15
config.ts 96.15 100 0 96.15 37-39
test/utilities 100 50 100 100
testDatabases.ts 100 50 100 100 2
------------------------- --------- ---------- --------- --------- ------------------------------------------------------

Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 8.422 s
Ran all test suites.

🏁 Tests complete!

今回は、すべて (SQLite / MySQL / Maria / PostgreSQL) の DB に対してテストを流しています。が、流石に毎度実行すると時間も CI のリソースも消費してしまいますよね。。そのときは以下のように、実行時にテストするものだけをパラメータとして渡して、省力化しておくとよいでしょう。

testDatabases.ts
export const allDatabases = ['sqlite3', 'mysql', 'maria', 'postgres'];
export const testDatabases = process.env.TEST_DB?.split(',').map((v) => v.trim()) ?? allDatabases;

自分がつくっているヘッドレス CMS では、ロジックをいじったときは全ての DB それ以外は SQLite のみにしています。

TEST_DB=sqlite3 yarn test:int

GitHub Actions

無事ローカルでテストを実行できたので、GitHub Actions のワークフローとして登録していきましょう。繰り返しになりますが、テストを自動化しておくことで、ちょっとした修正や renovate によるライブラリのアップデートなどで、思わぬバグに見舞われることを軽減できます。

jobs:
  integration:
    name: ${{ matrix.database }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        database:
          - sqlite3
          - mysql
          - maria
          - postgres
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Yarn install
        run: yarn install

      - name: Start Services
        if: matrix.database != 'sqlite3'
        run: docker compose -f test/docker-compose.yml up ${{ matrix.database }} -d --quiet-pull --wait

      - name: Run Tests
        run: TEST_DB=${{ matrix.database }} yarn test:int

CI でも PR ごとにテストする DB を絞っていきます。
やり方はいくつかあるでしょうが paths を指定して PR で修正したファイルがバックエンドコードだったり、テストコードそのものだったら全て走らせる条件を指定できます。

pull_request:
  branches:
    - main
  paths:
    - src/api/**
    - test/api/**
    - package.json
    - .github/workflows/integration-full.yml

それ以外は、Docker の立ち上げが不要な SQLite だけで動かすなど時間と CI のリソースを節約していきましょう!

最後に

React / Node.js / RDB で動くオープンソースのヘッドレスCMS 『 Superfast 』 をつくっています 💪

マークダウンではGFM (GitHub Flavored Markdown) が使えたりダークモードに対応したり、エンジニアが書き心地のよいCMSを目指しています。Live Demo も公開していますので、ぜひ気軽に触ってみてください!

そして、気に入って頂けた方は、ワンラインでのローカル起動も試してみてください 😻

npx create-superfast-app my-app

それでは Happy Testing!!

Collections

Discussion