【フロントエンド】JestからVitestへの移行結果とガイド
目次
TL;DR
Jest
からVitest
へのテストランナーの移行を行い、全体のテスト時間が大幅に短縮されました。GitHub Actions
でのUsageRunTimeは、Jest
の32m35sに対してVitest
は18m47sと、約14mの短縮が見られました。- ローカルテストの実行時間は最大210sの短縮が見られました。
前提・対象とする読者
- テストの効率化とパフォーマンス改善を目指している開発者
Jest
からのテストランナーの移行を検討している開発チーム
比較結果詳細:Jest vs Vitest
※ 比較時の前提条件として以下がありますのでご了承ください。テストケースの合計が1800件弱となっているためクリティカルに比較結果に影響しないものと判断して掲載しています。
- JestからVitestへ移行する際、実装予定のない機能のテストケースを移行時に削除しました。そのため総テスト件数が19件ほど異なります。
- JestからVitestへ移行する際、実行結果が不安定なテストに関しては一時skip対応を行ないました。そのためJestのテストのpass件数が23件多くなっています。
GitHub Actions
項目 | Jest | Vitest | 短縮時間 |
---|---|---|---|
Usage | 32m35s | 18m47s | 約14m |
Duration | 8m10s | 3m46s | 約4.5m |
※ Usageは各テストの実行時間の合計(並列を直列実行にするイメージ)で料金に直結し、Durationはテストの体感時間を示しています。
※ 画像はUsage比較
ローカルテスト
項目 | Jest | Vitest | 短縮時間 |
---|---|---|---|
全体テスト | 345s~379s | 169s~186s | 約160s |
🔎 ローカルテストアウトプット比較
Jest(sequential & parallel)
※ Jestでは実行結果が不安定なテスト防止に並列実行と直列実行が混ざっています。
※ 🌱 は体感時間
=== 合計:230 + 149 = 379s
Test Suites: 58 passed, 58 total
Tests: 42 skipped, 68 todo, 1421 passed, 1531 total
Snapshots: 0 total
Time: 🌱230.462 s(parallel)
Test Suites: 8 passed, 8 total
Tests: 18 skipped, 12 todo, 236 passed, 266 total
Snapshots: 0 total
Time: 🌱149.118 s(sequential)
===
=== 合計:212 + 150 = 362s
Test Suites: 58 passed, 58 total
Tests: 42 skipped, 68 todo, 1421 passed, 1531 total
Snapshots: 0 total
Time: 🌱212.438 s
Test Suites: 8 passed, 8 total
Tests: 18 skipped, 12 todo, 236 passed, 266 total
Snapshots: 0 total
Time: 🌱150.576 s
===
Vitest
Test Files 73 passed (73)
Tests 1634 passed | 81 skipped | 63 todo (1778)
Start at 16:13:21
Duration 🌱178.36s (transform 2.60s, setup 41.21s, collect 61.93s, tests 578.47s, environment 16.18s, prepare 4.28s)
Test Files 73 passed (73)
Tests 1634 passed | 81 skipped | 63 todo (1778)
Start at 16:16:55
Duration 🌱169.16s (transform 2.71s, setup 41.15s, collect 54.73s, tests 556.54s, environment 8.55s, prepare 4.38s)
Test Files 73 passed (73)
Tests 1634 passed | 81 skipped | 63 todo (1778)
Start at 17:33:02
Duration 🌱186.11s (transform 4.97s, setup 80.57s, collect 115.89s, tests 1056.40s, environment 14.76s, prepare 6.79s)
💨移行理由
移行理由はテストの実行時間が開発体験と開発コストに支障をきたしていたためです。
前提として私が所属するプロジェクトのテストケースは1500超に達しローカルのテスト実行に5~6m、git commit
のたびにpre-commit
のテスト実行による待ち時間の発生、CIの実行待ち時間が10m弱と諸々の理由で開発体験が悪くなっていました。
また他のプロジェクトに比べてGitHub Actions
の利用時間を多く使っており、このまま実装を進めていくことでコストが増え続ける懸念がありました。
📖移行ガイド
1. パッケージのインストール
以下のパッケージをnpm
等でインストールします。
※ 移行時点でのバージョンを記載していますのでお手元の環境に合わせて変更してください。
"happy-dom": "^13.7.8", // node環境でDOM環境を再現
"vitest": "^1.3.1", // vitest本体
"@vitejs/plugin-react": "^4.2.1", // Reactコンポーネント利用時に必要
"@testing-library/jest-dom": "6.4.2", // Matcherの拡張
2. 設定ファイル
Managing Vitest config file
Configuring Vitest
基本的なことは公式ドキュメントに書いてありますので特筆すべきところのみ紹介します。
- environment: テスト環境を指定します。DOM環境を再現したい場合は
js-dom
やhappy-dom
を指定します。これによってテスト内でdocument.querySelector
やtesting-library等のDOM用の取得メソッドが使用できるようになります。 - globals:
Jest
のようにグローバルにメソッド(describe
,it
, etc…)を利用したい場合指定が必要です。指定しない場合、CLIで引数を渡すか、vitestから明示的にimportして使用する必要があります。 - alias: テスト実行対象ファイル内で参照しているモジュールのパス解決を行います。
Nextjs
などでパスエイリアスを設定している場合は指定が必要になります。
他にもsetupファイルの指定やtimeoutの指定等可能なのでご自身の環境に応じて追加で設定してみてください。
export default defineConfig({
plugins: [react()],
test: {
environment: 'happy-dom',
globals: true,
alias: {
'@': path.resolve(__dirname, '../src/'),
},
},
});
余談ですが、私が所属するプロジェクトではDOM環境の再現にhappy-domを使用しています。DOM環境の再現用パッケージはいくつかありますが、js-dom
かhappy-dom
の2択になっている印象です。happy-dom
はjs-dom
に比べて使用できるAPIが少ないが高速という特徴があります。現在プロジェクトのフロントのテストケースは1500超ですがとくに不便は感じておらず、快適にテストライフを送ることができています。
3. グローバルな型補完
tsconfig
に以下の設定を追加することでvi.mock
等の型補完を効かせることが可能です。
"compilerOptions": {
...,
"types": [
...
"vitest/globals",
]
},
4. マッチャーの拡張
Vitest
のマッチャーにはデフォルトではtoBeInTheDocument
等は用意されていません。これらのマッチャーを使用したい場合マッチャーの拡張を行う必要があります。
デフォルトで使えるマッチャーは以下を確認ください。
設定は以下を参考にしても行えます。
Vitest
のsetupファイルに以下を追記します。
※ setupファイルは任意のディレクトリに作成してください。例:src/test/setup.ts
import '@testing-library/jest-dom/vitest';
setupファイルはvitest.config.ts
ファイルに読み込ませます。
export default defineConfig({
plugins: [react()],
test: {
...,
setupFiles: ['/path/to/setup.ts'],
},
});
最後にマッチャー用の型を拡張してください。グローバルにマッチャーの型補完が効くようになります。
"compilerOptions": {
...,
"types": [
...
"@testing-library/jest-dom",
]
},
※ マッチャーの型情報がjest.Expect
になりますがこのライブラリを使用して拡張する以上仕方ないかと思います。
5. 環境変数の設定
Vitest
ではvitest.config.ts
ファイルやCLIでの引数指定が可能です。
今回はテストファイル内で環境変数を参照したいときという要件に絞って紹介します。
グローバルセットアップファイルではテスト実行の最初と最後に指定の関数が実行されます。それを利用して環境変数に値をセットします。
type Env = {
[key: string]: string;
};
export const env: Env = {
EXECUTION_BY: 'local',
};
Object.keys(env).forEach((key) => {
process.env[key] = env[key];
});
/**
* INFO: setupとteardownのexportが必須のためconsoleのみ実行
*/
export const setup = (): void => {
console.log('Global Setup was called.');
};
export const teardown = (): void => {
console.log('Global Teardown was called.');
};
私の所属するプロジェクトではローカルではパスするが GitHub Actions
等のCIでは落ちてしまう不安定なテストが存在します。その不安定なテストをCIでスキップするために環境変数でローカル実行なのかCI実行なのかわかるようにしています。
たとえば以下のように使用します。
// EXECUTION_BYの値がlocalの場合実行
it.runIf(process.env.EXECUTION_BY === 'local')(
'テストケース',
async () => {
//...
}
);
※ 以下のようにconfigファイルでもenvを設定できますが、テストファイルの中で参照することができません。申し訳有りませんが理由は調査しても見つけきれませんでした。
export default defineConfig({
...,
test: {
...,
env: {
EXECUTION_BY: 'local'
},
},
});
🚧移行過程での課題
テストの謎落ち
インプットの入力が絡むテストとStoryBook
が絡むテストで謎に落ちるテストが稀にありましたがテストを別ファイルに分割することで解決しました。
以下のissueは関連していそうですがはっきりとした理由はまだわかっていないため引き続き調査が必用です。
testing-library/react: cleanup not called automatically with isolate: false
余談ですが、私のチームではStorybook
をテストで使用していました。play
メソッドでStoryの振る舞いをテストで再利用しているイメージです。Storybook8ではVitest
サポートが強化され、storybook/testも公開されたためこれらを利用するように移行も行いました。
GitHub Actionsのキャッシュ残り
移行後CIでテストを実行した際にテストが失敗してしまう事象が発生しました。よくよく確認するとJest
時代のnode_modules
のキャッシュが残っておりパッケージはそこを参照していたためでした。キャッシュからnode_modules
を削除することで対応しました。
以上になります。同じような移行を検討している他のプロジェクトチームにとっての参考になれば幸いです。