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 --runInBand
か jest --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.json
やjest.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.json
のscripts
に実行用のスクリプトを用意しても良いです。
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でのフロントエンド開発をしています! エンジニアの採用を積極的に行なっているので、ご興味のある方はぜひ!
参考文献
この記事は以下の情報を参考にしました。