EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

Jestで並列実行するテスト群と逐次実行するテスト群を分けて実行できるようにする

1. はじめに

こんにちは!株式会社エモーションテックで開発インターンをしている渋谷です! テスティングフレームワークJestにはさまざまな利点がありますが、中でもテストを並列で高速実行してくれる機能は開発体験を大幅に向上してくれます。

しかし、DBにアクセスするテストやE2Eテストなど副作用を持つテストは、並列実行されると他のテストケース次第で取得する値や状態が変化してしまい、ランダムで失敗してしまうことがあります。 全てのテストを並列ではなく1つずつ逐次実行すればテストは安定しますが、テストの実行速度は格段に落ちてしまいます。

そこで今回は、他のテストケースの影響を受けて欲しくないテストは逐次実行して、そうでないテストは並列実行することで、テストの実行速度は最大限保ちつつ、テストを安定化させる方法について書いていこうと思います!

2. Jestテストの並行実行について

Jestではデフォルトでテストを実行するとファイルは並列実行、ファイル内のテストケースは逐次実行されます。 https://qiita.com/noriaki/items/5d800ea1813c465a0a11

上記の記事で細かく解説されています。

デフォルト状態でJestを実行すると、対象となる複数のテストファイルは並行に処理されます。このときの並行処理の上限数は、自動的に実行環境(マシン)のコア数が設定されます。

この上限数を変えるには--maxWorkers=:numberをCLIオプションに指定します。ただ、コア数以上の数字を指定してもパフォーマンス上の効果は薄く、通常は書き換えない方が良いとjest --helpには書かれています。

--maxWorkers, -w Specifies the maximum number of workers the worker-pool will spawn for running tests. This defaults to the number of the cores available on your machine. (it's usually best not to override this default) また、ファイル内のテストはテストメソッドit、testを使用すると順番に実行されます。

describeでブロック化されている場合は、describeに潜っていく(深さ優先探索の)順序で実行されます。かんたんに言えば、テストファイルに書かれている順序です。

ファイルも含めて全て逐次実行したい場合は、jest --runInBandjest --maxWorkers=1で実行することができます。

https://jestjs.io/ja/docs/cli#--runinband

https://jestjs.io/ja/docs/cli#--maxworkersnumstring

3. 実装

テストファイルの分類

まずテスト群を分類するために逐次実行したいファイルの接尾語に特定の単語を追加します。 今回は「一連の」や「連続的」という意味を持つ「sequential」を使います。

NestJSのspecファイルの場合...

app.controller.spec.ts
           ↓
app.controller.sequential.spec.ts

specとtsの間に単語を入れるとデフォルトのjestがテストを走らせるときに使うパターンマッチと合わなくなったりするので気をつけてください。

package.jsonjest.config.jsを見ると確認できます。

package.json

{
  ...
  "jest": {
    ...
    "testRegex": ".*\\.spec\\.ts$", // この場合は ファイル名.spec.ts にマッチします
    ...
  }
}

分割して実行する

次に「sequential」をつけたファイルを逐次実行、つけてないものを並列実行と分けて実行できるようにします。

実行できるようにするには、configファイルを2つ定義する方法とprojectsを分ける方法の2つがあります。

a. configファイルを2つ定義して実行する

JestのCLIオプション --config = ファイル名 を指定することでconfigファイルを指定して実行することができます。https://jestjs.io/ja/docs/cli#--configpath そこで逐次実行用のconfigファイルと並列実行用のconfigファイルを定義します。

逐次実行 config

jest-sequential.json

{
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "../src",
  "testRegex": ".*\\.sequential\\.spec\\.ts$", // 「ファイル名.sequential.spec.ts」 にマッチ
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "testEnvironment": "node",
  "maxWorkers": 1 // maxWorkerを1に指定して逐次実行するようにする
}

逐次実行ではtestRegex等を使って「ファイル名.sequential.spec.ts」のテストファイルだけを対象にします。 また、maxWorkersを1に指定して逐次実行されるようにします。

jest --config = ./configがあるパス/jest-sequential.jsonで実行できます。

並列実行 config

jest-parallel.json

{
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "../src",
  "testRegex": "^(?!.*\\.sequential\\.spec\\.ts$).*\\.spec\\.ts$", // 「ファイル名.sequential.spec.ts」 以外にマッチ
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "testEnvironment": "node"
  // maxWorkersはデフォルト(or 2以上)
}

並列実行ではtestRegexを使って「ファイル名.sequential.spec.ts」のテストファイルは除外しましょう。 maxWorkersはデフォルト(or 2以上)で並列実行されるようにします。

まとめて実行させる

jest --config = ./configがあるパス/jest-sequential.json && jest --config = ./configがあるパス/jest-parallel.json

package.jsonscriptsに実行用のスクリプトを用意しても良いです。

package.json

{
  ...
  "scripts": {
    ...
    "test": "npm run test:parallel && npm run test:sequential",
    "test:parallel": "jest --config ./test/jest-parallel.json",
    "test:sequential": "jest --config ./test/jest-sequential.json",
    ...
  },
  ...
}

実際に実行すると以下のようになります。

ファイル名.sequential.spec.tsのファイル群は逐次実行されていて、それ以外は並列実行されていますね。

問題点

ちょっとした点ですが、2つに分けて実行されているため結果も分かれて表示される形になっています。

b. projectsを分けて実行する

こちらの方法だと1つの結果として表示されるのでその点では良いです。(ただし別の問題あり)

Jestの設定ではprojectsに複数のconfigを配列で記述することができます。 https://jestjs.io/ja/docs/configuration#projects-arraystring--projectconfig そこで以下のように定義します。

package.json

{
  ...
  "jest": {
    ...
    "projects": [
      {
        "displayName": "sequential",
        "moduleFileExtensions": [
          "js",
          "json",
          "ts"
        ],
        "rootDir": "./src",
        "testRegex": ".*\\.sequential\\.spec\\.ts$", // 「ファイル名.sequential.spec.ts」 にマッチ
        "transform": {
          "^.+\\.(t|j)s$": "ts-jest"
        },
        "testEnvironment": "node",
        "runner": "../runner/jest-sequential-runner.js" // maxWorkersの代わりにrunnerを指定
      },
      {
        "displayName": "parallel",
        "moduleFileExtensions": [
          "js",
          "json",
          "ts"
        ],
        "rootDir": "./src",
        "testRegex": "^(?!.*\\.sequential\\.spec\\.ts$).*\\.spec\\.ts$", // 「ファイル名.sequential.spec.ts」 以外にマッチ
        "transform": {
          "^.+\\.(t|j)s$": "ts-jest"
        },
        "testEnvironment": "node"
      }
    ]
  }
}

こちらの逐次実行configではmaxWorkersの代わりにrunnerを指定して逐次実行させています。(詳細後述) https://jestjs.io/ja/docs/configuration#runner-string

rootdir/runner/jest-sequential-runner.js

const TestRunner = require('jest-runner').default

class SerialRunner extends TestRunner {
  constructor(...attr) {
    super(...attr)
    this.isSerial = true
  }
}

module.exports = SerialRunner

このrunnerではisSerialをtrueにしてテストを逐次実行しています。 jest-serial-runnerというrunnerが公開されていますが、対応しているのが"jest-runner": ">= 24.8.0"までだったり、#16のようなissueが報告されているのでこうしています。

まとめて実行させる

projectを指定して実行するときは--selectProjectsを使います。 https://jestjs.io/ja/docs/cli#--selectprojects-project1--projectn

jest --selectProjects parallel sequential

こちらでもpackage.jsonスクリプトを定義できます。

package.json

{
  ...
  "scripts": {
    ...
    "test:": "jest --selectProjects parallel sequential",
    "test:parallel": "jest --selectProjects parallel",
    "test:sequential": "jest --selectProjects sequential"
    ...
  },
  ...
}

実行すると以下のようになります。

先述の通り、結果がまとめて表示されていますね。 もちろん、逐次実行と並列実行は分けて実行されています。

問題点

projects下のconfigではmaxWorkersが指定できないため、runnerを指定する方法をとっています。 #3215#10936などで"maxWorkers": 1"runInBand": trueを定義できるようにすることが議論されていて、https://github.com/facebook/jest/pull/10912のように実装することが提案されていますが、2022/08/15現在使用することができません。

そのためrunnerを指定しましたが、今回のrunnerの実装もTestRunnerの基盤クラスBaseTestRunnerのreadonlyプロパティを書き換えるという方法なのであまりいい方法とは言えないかもしれません。

結論

現時点でのprojects方式はあまり綺麗な方法ではなく、テストを分けて実行するconfigファイル方式は特にデメリットがないので、弊社のプロダクトではaのconfigファイル方式を採用しています。

今後、projects下のconfigでmaxWorkerかrunInBandを記述する方法が公式にサポートされた場合はprojects方式でも問題ないと思います。

4. 終わりに

今回はJestテスト群を分けて実行する方法について書きました。 皆さんの開発におけるテストの安定化につながれば幸いです。

弊社ではNestJSを用いたバックエンド開発やAngularでのフロントエンド開発をしています! エンジニアの採用を積極的に行なっているので、ご興味のある方はぜひ!

hrmos.co

hrmos.co

hrmos.co

参考文献

この記事は以下の情報を参考にしました。