目次
- 背景
- jest,ts-jestとは
- testのタイミング
- 検証環境
- 検証内容
5.1. runInBand
5.2. max-workers
5.3. isolatedModules
5.4. lastCommit,onlyChanged
5.5. CIでJest cacheの保存・リストア
5.6. CI自体の並列実行
5.7. jest-light-runner
5.8. swc/jest
5.10. esbuild-jest
5.11. jest configファイルをjs、jsonにする - 検証結果
6.1. runInBand
6.2. max-workers
6.3. CIでJest cacheの保存・リストア
6.4. tsc + swc/jest
6.5. jest configファイルをjs、jsonにする - その他の改善案
- ts-jestが今後高速化する?
- 最後に
1. 背景
弊社ではフロントエンドのテストでjest,ts-jestを採用しています。
テストコード、ファイルの量が増えるに連れてテストの実行時間が膨大になるのは自明です。
そこで、少しでも実行時間を短縮したいなと思ったからです。
2.jest,ts-jest
jest
- JavaScriptのテストフレームワークです。
- テストランナー、アサーション、テストモック・テストダブルがオールインワンになっています。
3.testのタイミング
- リモートにpush時
- ローカルで好きなタイミングで実行
ts-jest
- TypeScriptで書いたテストコードを、実行前にJavaScritpにトランスパイルします。
- 型チェックを含むTypeScriptのすべての機能をサポートしています。
4. 検証環境
こちらのリポジトリで行います。
5.検証方法
5.1. runInBand
runInBandとは
1つのプロセスですべてのテストをシリアル(順番)に実行します。
Jestは、高速なSSDを搭載した最新のマルチコアコンピュータ上ではほとんどの場合非常に高速ですが、特定のセットアップ上では遅くなることがあります。
速度を最大50%改善する1つの方法は、テストを連続して実行することです。
これを行うには、--runInBand を使って同じスレッドでテストを実行します
# Using Jest CLI
jest --runInBand
# Using your package manager's `test` script (e.g. with create-react-app)
npm test -- --runInBand
5.2.max-workers
max-workersとは
- テストの実行時にワーカープールが生成するワーカの最大数を指定します。
- シングルランモードでは、マシンで使用可能なコア数からメインスレッド用に1を引いた数がデフォルトです。
- CIのようなリソースが限られた環境ではこれを調整するのが便利かもしれませんが、ほとんどのユースケースではデフォルトで十分だそうです。
- 利用可能なCPUが変化する環境では、パーセンテージベースの設定もできます。
ローカル環境下では--maxWorkers=49%という記事を発見
github-actionsでの使用例
github-actions-cpu-coresでCPUの数を検出し、それをJestに渡します。
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v2
- name: run tests
run: npm jest --max-workers ${{ steps.cpu-cores.outputs.count }}
5.3 isolatedModules
ts-jestが使用するtsconfigのincludeの値を変更することで、このモードを使用する際のパフォーマンスを向上させる方法があります。
includeで提供されるファイルが少なければ少ないほど、テスト実行のパフォーマンスが向上します。
複数のファイルをimportしてテストするのが多いのでこちらは却下とします。
5.4 lastCommit,onlyChanged
ミツモア様のブログに以下の記述がされていましたので却下とします。
修正数に応じてテスト時間にばらつきがでたり、 多くのファイルに依存しているファイルを編集するとほとんどのファイルが実行されたり、 そしてごく稀に直接依存しないテストで不具合が見つかったりしたこともあった(例: node の version up でうまく動かなくなったファイルなど)ため、 現在ではこのオプションは利用せず全実行させるようにしています
5.5. CIでJest cacheの保存・リストア
以下2つの記事を参考に実装します。
CI で cache しやすいようにcacheの配置先を cacheDirectory で明示的に指定します。
cacheDirectory とは Jest がキャッシュされた依存情報を保存するディレクトリです。
// ...
cacheDirectory: "node_modules/.cache/jest"
// ...
}
name: Test CI
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: cache jest
uses: actions/cache@v3
with:
path: node_modules/.cache/jest
key: jest-v1-${hashFiles('package-lock.json')}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
jest-v1-
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v2
- name: Run Jest tests
run: npx jest --max-workers ${{ steps.cpu-cores.outputs.count }}
5.6 CI自体の並列実行
以下の記事にも書いてある通り、job自体の実行時間の短縮はされないので却下とします
github actions 上のテストを並列化する
github actions の実行時間無料枠の消費は job 単位(runs-on単位)となるため、パイプラインとしての実行時間は短縮されるが無料枠の消費時間は変わらない。
5.7 jest-light-runner
環境を仮想化することなく、素のNode.jsで直接テストを実行するJestランナーです。
使い方は以下のようにpackage.jsonか、jest.configを修正してください。
{
"jest": {
"runner": "jest-light-runner"
}
}
module.exports = {
runner: "jest-light-runner",
};
記事で以下のように書かれてました。
全てのjest機能がこのランナーでサポートされているわけではないです。
最もよく使われる機能は全て動作します。
実際に何が使えて何が使えないのかはjest-light-runnerの公式に書いてました。
他に欠けているJest機能があるかもしれないとのことです。
以下の理由で不採用とします。
- jestのゼロコンフィグの良さが失われる
- コミュニティが小さい中で設定をカスタマイズして運用していく(テストを独立して実行させたり、mockのimport等)のが難しそう
興味はあるので実際どのくらい速くなるのか今度検証してみます🙏。
5.8. swc/jest
npm i --save-dev @swc/jest
transform: {
'^.+\\.ts$': ['@swc/jest'],
}
以下の問題点があります。
- 型チェックがされない
- spyonが使えない
spyonを使えるようにする設定につきましては別記事で投稿します🙏。
型チェックをするように設定します。
tsconfig.jsonの主要オプションを理解する
以下です。
"scripts": {
"tsc-swc-jest": "tsc --noEmit && npx jest"
}
tscで型チェック、トランスパイルはswcで行うという流れです。
import { processNumbers } from "../src/processNumbers";
describe("processNumbers", () => {
it("should double each number in the array", () => {
const numbers = [1, 2, 3];
const result = processNumbers(numbers);
expect(result).toEqual([2, 4, 6]);
});
it("should return an empty array if no numbers are provided", () => {
const numbers: number[] = [];
const result = processNumbers('');
//tscの型チェックでここがエラーになります。
expect(result).toEqual([]);
});
});
~/Desktop/github.com/yamatai12/jest-lab(test/swc-jest) $ npm run tsc-swc-jest
> jest-lab@1.0.0 tsc-swc-jest
> tsc --noEmit && npx jest
tests/processNumbers3.test.ts:12:35 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number[]'.
12 const result = processNumbers('');
~~
Found 1 error in tests/processNumbers3.test.ts:12
確かにエラーになりました。
この設定で検証します。
5.9. jest configファイルをjs、jsonにする
JestでTypeScriptを高速化する
こちらの記事でconfigファイルをtsからjs,json形式にした方が速いと記載されていました。
トランスパイルする時間がconfigファイル分のみ短縮されるのであまり大きな効果は見込めないとは思いますが、検証します。
5.10. esbuild-jest
最近のバージョンアップ履歴が3年前なのも考慮して、esbuild-jestは不採用とします。
6.検証結果
6.1. runInBand
ローカル
1回目 | 2回目 | 3回目 | |
---|---|---|---|
runInBand | 30.187s | 30.028s | 30.293s |
defalut | 7.919s | 7.562s | 7.535s |
平均 |
---|
30.169s |
7.672s |
むしろ実行時間が増えたので runInBand は不採用 とします。
6.2. max-workers
ローカル --maxWorkers=49%
1回目 | 2回目 | 3回目 | |
---|---|---|---|
maxWorkers=49% | 8.201s | 8.382s | 8.308s |
defalut | 7.919s | 7.562s | 7.535s |
平均 | |
---|---|
maxWorkers=49% | 8.297s |
defalut | 7.672s |
むしろ実行時間が増えたので--maxWorkers=49% は不採用 とします。
github-actions
1回目 | 2回目 | 3回目 | |
---|---|---|---|
maxWorkers=github-actions-cpu-cores | 2m17s | 2m14s | 2m26s |
defalut | 2m35s | 3m46s | 3m18s |
平均 | |
---|---|
maxWorkers=github-actions-cpu-cores | 139s |
defalut | 193s |
jest公式documentの設定にすると、
github-actionsでの実行時間が短縮されることを確認できました。
6.3. CI で Jest cache の保存・リストア
6.4. tsc + swc/jest
ローカル
1回目 | 2回目 | 3回目 | |
---|---|---|---|
swc/jest | 5.86s | 5.742s | 5.591s |
defalut | 7.919s | 7.562s | 7.535s |
平均 | |
---|---|
swc/jest | 4.3602s |
defalut | 7.672s |
短縮されました。
github-actions
1回目 | 2回目 | 3回目 | |
---|---|---|---|
maxWorkers=github-actions-cpu-cores + tsc + swc/jest | 1m59s | 1m29s | 1m19s |
maxWorkers=github-actions-cpu-cores | 2m17s | 2m14s | 2m26s |
平均 | |
---|---|
maxWorkers=github-actions-cpu-cores + swc/jest | 96s |
maxWorkers=github-actions-cpu-cores | 139s |
短縮されました。
6.5. jest configファイルをjs、jsonにする
ローカル
1回目 | 2回目 | 3回目 | |
---|---|---|---|
js | 7.404s | 7.383s | 7.439s |
defalut | 7.919s | 7.562s | 7.535s |
平均 | |
---|---|
js | 5.6382s |
defalut | 7.672s |
github-actions
1回目 | 2回目 | 3回目 | |
---|---|---|---|
maxWorkers=github-actions-cpu-cores+config-js | 2m21s | 2m23s | 1m40s |
maxWorkers=github-actions-cpu-cores | 2m17s | 2m14s | 2m26s |
平均 | |
---|---|
maxWorkers=github-actions-cpu-cores+config-js | 128s |
maxWorkers=github-actions-cpu-cores | 139s |
- 大した差はなかったです。
- configファイルをts化する方がメリットが大きい(型チェックによる設定誤りの防止)ので不採用とします。
7. その他の改善案
- ファイル単位で遅いテストを改善する
- 全テストケースで共通的に実行される処理の見直し
- テスト戦略の見直し
- 例)テストはとにかく速さ重視
8. ts-jestが今後高速化する?
そのような記事を見つけました。
ts-jest が esbuild/swc をトランスフォーマーに使って高速化していた
Support for esbuild or swc
以下のようなissueが上がっていて、プルリクは作成されてないのでまだ着手されてないようです。
CHANGELOGにもそのような文言は見当たりません。
9. 最後に
local
- defaultの設定のまま
github-actions
- jest公式documentの設定にする
- jestのcacheを使用する
swc/jestをtransformerに設定するのは以下を満たせば良いかなと思いました。
- テスト戦略的に速さを重視するのか
- swc/jestにすることでjestで使えない機能が出てしまうことによる工数の増加、テスト実装の柔軟性が失われるのか
- 本番環境と異なるトランスパイラを使用することによるテスト漏れ、バグの検証で時間がかかる可能性が低いのか
- トランスパイラを複数使うことによる運用工数があまりかからないのか
弊社は本番でvite(esbuild)、テストでts-jest(tsc)を採用しています。
swc/jest、ts-jestによるテストコードの書き換えが特にないのであれば、テストでswc/jestを採用しても問題なさそうです。
また、使用する際は tsc --noEmitを実行してからswcでトランスパイルする方法が型チェックもできて安全かなと思います。
ts-jestが高速化したら、ts-jestに戻すのもありだなと思いました。
vitestに乗り換えるメリットが大きいかは次回調査します。
参考になる記事を書いてくださった皆様に感謝致します!
参考
- Jestでテストを書こう
- preset: ts-jest とは
- jest transform
- No speed difference after replacing ts-jest with @swc/jest?
- 環境の準備とテストの実行
- Monorepo下におけるJest実行方法のちょっとした改善
- jestのテストが遅い場合に確認すべきこと
- https://www.quora.com/Are-logical-cores-always-double-that-of-physical-cores
- https://betterprogramming.pub/how-to-improve-the-jest-performance-in-ci-environments-when-using-typescript-66a186cb5cd4
- https://kazumaxneo.hatenablog.com/entry/2020/06/12/233000
- https://www.mankier.com/1/hyperfine
- Make your Jest Tests upto 10x Faster
- CI/CD — Improve The Testing Time in Jest and GitHub Action with 90% Effectiveness
- Improve Jest Runner Performance
- How we sharded our test suite for 10x faster runs on GitHub Actions
- CI 環境でのユニットテストの実行時間を2倍速くした話 (Jest + Mongo DB + Circle CI)
- Github Actions のテスト実行時間を速くするためにやったこと
- Jestテストの並行実行と逐次実行をちゃんと理解する
- テストCI実行時間を半分にした話
- JestでTypeScriptを高速化する
- CircleCI上のフロントエンドテストの実行時間を30分から10分に短縮した話
- 自動テスト速度改善 - 自動テストが品質のボトルネックとならないために
- Jest v28 shard オプションを使い、CI でカバレッジを計測できるようにする
- NestJS 製のプロジェクトのテストを@swc/jest で高速化する
- A practical guide to removing frustration with Jest and Jenkins
- Write Parameterized Tests In Jest
- GitHub Actionsをローカルでテストする方法
- GitHubActionsで自動ビルドする際、Tokenのスコープでつまづいた話
- How to speed up Jest tests
- JestのcacheをCIでも利用して高速化するメモ
- github actionsのjobを高速にするために取った対策
- Should you use jest as a testing library?
- jest-light-runner
- TypeScript + SWC + Jestでexport方法別のspyOnするパターンを書いた
- tsconfig.jsonの主要オプションを理解する
- Jestを使ったテスト高速化のため、ts-jestではなく、@swc/jestを使ってみたら予想以上に速かった話
- GitHub Actionsをローカルでテストする方法を理解する