👻

【Jest】テストコマンドが異様に長かったのでjest.config.jsを使って見直しした話

2023/12/17に公開

環境

  • TypeScript: 4.8.2
  • jest: 28.1.3 -> 29.7
  • node: 18.x -> 18.18

背景

ある日突然、CIテストが落ちて謎のエラーが...

プロジェクトに参画して数ヶ月経ち...
いつも通りのんびり開発していたところ、私が出していたプルリクのCIテストが落ちた。

ログを見てみると「TypeError: Cannot assign to read only property 'performance' of object '[object global]'」というエラー。
performanceというプロパティも利用していなかったので身に覚えがなかった。

調査した結果、原因は useFakerTimersを使った際に、nodeのバージョンが18.19以降だとエラーが起こる というバグのせいだとわかった。偶然プロジェクト内のnodeのバージョンが上がったタイミングだったのでテストが落ちていたのだ。

こちらは結果的に、nodeを18.18に固定することでエラー解消できた。
よかったよかった。

次はメモリ不足エラー。解決するには長いテストコマンドをさらに長くしないといけなくなり...?

修正してプルリクを出すと、またCIのテストでエラーが発生していた。
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory」というエラーだ。

ふむふむ。よくあるメモリ不足の問題だな。
前職でも起こったエラーだったので NODE_OPTIONS='--max-old-space-size=4096'を設定すれば解決すると知っていた。

上記を、package.jsonにある既存のテストコマンドに加え、解決はしたのだが...

package.json
"test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings --max-old-space-size=4096' jest --testTimeout=5000 --verbose --forceExit --maxWorkers=1 --runInBand"

な、、、長すぎる、、、!

なんだこれは...!

唯一スクロールしないと全貌が見れないコード!
未経験エンジニアが見たら一瞬で眠たくなる魔法の呪文!
長くても改行できない、eslintも効かないこの図太さ!

私はどうしてもこの長文コマンドが好きになれなかった。なぜこいつだけ異様に長くなるのか...

私はコマンドに対し、「こんなに出過ぎた杭(コマンド)は地の果てまで打ち込んで(短くして)やろう」と、そう心に誓い動きはじめた。

第1施策:必要コマンドオプションの洗い出し

はい。技術記事名ので真面目にやります。

まず、現状のコマンドオプションに対して、プロジェクトに沿った必要なものを洗い出してみます。

オプション 必要か 理由
--experimental-vm-modules 不要 今のプロジェクトではESM を使っていなかったため削除
--no-warnings 不要 警告メッセージは不要なので削除
--max-old-space-size=4096 必要 メモリ不足エラー対策のため
--testTimeout=5000 不要 デフォルト設定のため削除
--verbose 必要 プロジェクトの意向で、詳細は出力するため
--forceExit 必要 非推奨だが、CIで終了させる必要があるため仕方なく...
--maxWorkers=1 必要 逐次実行したいため
--runInBand 不要 --maxWorkers=1 と同じ意味なので削除

意外と不要なものがたくさんありましたので、既存コマンドからこちらを削除しました。
少しは短くなりましたが まだまだ長いですね...

-"test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings --max-old-space-size=4096' jest --testTimeout=5000 --verbose --forceExit --maxWorkers=1 --runInBand"
+"test": "NODE_OPTIONS='--max-old-space-size=4096' jest --verbose --forceExit --maxWorkers=1"

第2施策:コマンドオプションをjest.config.jsへ移行

残っているコマンドオプションですが、jest.config.jsに設定を移行することでさらに短くすることができそうです。
参考:Jest CLIコマンドドキュメント

また、NODE_OPTIONS='--max-old-space-size=4096'についても、jestのバージョンを29系に上げることで、jest.config.jsにworkerIdleMemoryLimitを設定して回避することができるというのがわかりました。

では早速jestのバージョンをあげます。
参画しているプロジェクトでは、28系からのバージョンアップだったので特にエラーは出なかったです。

npm update jest@29.7

次にpackage.jsonファイルと同じ階層にjest.config.jsを作成し、下記のように設定しました。
それぞれの対応表は下記の通り。

CLIオプション jest.config.jsの設定 備考
--max-old-space-size=4096 workerIdleMemoryLimit: "1024MB" 設定した閾値に行くと自動的にメモリリサイクルされるようにした
--verbose verbose: true ログ出力を行う
--forceExit forceExit: true ドキュメントには書いてないが、jest.config.jsに書くと動作するとのこと。
--maxWorkers=1 maxWorkers: 1 最大ワーカー数を指定
なし preset: "ts-jest" TypeScript用のプリセット設定として必要
jest.config.js
module.exports = {
  workerIdleMemoryLimit: "1024MB",
  verbose: true,
  forceExit: true,
  maxWorkers: 1,
  preset: "ts-jest", // TypeScriptを使うので
};

これですべてのコマンドオプションの移行が完了し、
最終的にpackage.jsonのテストコマンドはこのようになりました。

-"test": "NODE_OPTIONS='--max-old-space-size=4096' jest --verbose --forceExit --maxWorkers=1"
+"test": "jest"

なんと、出過ぎた杭を地球の裏側まで埋め込んでやることに成功しました。
ここまできたらpackage.jsonから消しても問題なさそうなので、お好みで杭を消し去ることもできますね。

あとがき

正直、「メモリの設定はNODE_OPTIONだし、どうにもできないかなぁ...」と思っていたのですが、29系からやっとjest.config.jsで出来たのが本当に良かったです。やったね🐤

もしこの記事を読んだ方が、参画プロジェクトで呪文化しているテストコマンドを見た時はぜひjest.config.jsの設定を検討してみてはいかがでしょうか!

深夜テンション記事を読んでいただきありがとうございました。

参考ドキュメント

Jest CLIオプションドキュメント:https://jestjs.io/ja/docs/cli
jest.config.jsの設定ドキュメント:https://jestjs.io/ja/docs/configuration

Discussion