CI での Vitest vs Jest: 速度、ESM、移行
テストの実行時間はしばしば CI の最も長いステップです - Vitest と Jest は速度と ESM で大きく異なります。
どちらも JS/TS のテストを実行します。Vitest は Vite ネイティブで ESM ファースト、Jest は巨大なエコシステムを持つ定番の標準です。
Vitest vs Jest at a glance
| Jest | Vitest | |
|---|---|---|
| ESM サポート | ぎこちない (transform) | ネイティブ |
| 速度 | 良い | 多くの場合より高速 (Vite の transform) |
| 設定 | 成熟、多数のプリセット | 最小限、Vite に整合 |
| エコシステム | 最大 | 成長中、Jest 互換 API |
| 設定 | vite.config.ts を再利用 | 独立したJest設定とtransformチェーン |
| アサーション | Jest互換の expect を備えたChai | expect |
| watchモード | HMR風の再実行 | 標準のwatch |
| カバレッジ | v8またはIstanbul | babel-plugin-istanbulまたはv8 |
| ブラウザモード | あり | なし (jsdom/happy-domによる模擬のみ) |
| ベンチマーク / 型テスト | Tinybench、expect-type | なし |
| シャーディング | あり | あり。v28以降の --shard |
CI では
Vitest は概して高速で、Jest の "Cannot use import statement" 失敗を引き起こす ESM transform の頭痛を回避します。Jest は大規模で成熟したスイートには安全な選択肢のままです。
- Static
importstatements are evaluated before your code runs, so the hoistingjest.mock()depends on cannot happen under ESM. require()of an ESM file with top-level await throwsERR_REQUIRE_ASYNC_MODULE.- The
jestobject has to come from@jest/globalsorimport.meta.jestrather than being ambient. - Jest documentation notes the underlying Node APIs it uses for this are themselves experimental.
# Jest, ESM mode
NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest
# ...plus transform: {} in config (or a transformer emitting ESM),
# ...plus extensionsToTreatAsEsm for .ts/.jsx,
# ...and jest.mock() no longer works:
const { charge } = await import('./payments.js');
jest.unstable_mockModule('./payments.js', () => ({ charge: jest.fn() }));
# Vitest, no flags, no mode
vi.mock('./payments.js', () => ({ charge: vi.fn() }));どちらも OOM しうる
大きなスイートはどちらの runner でも CI ワーカーを OOM させます - いずれにせよワーカー数を制限し、メモリを適正化してください。
| Approach | Type-checks tests? | Cost |
|---|---|---|
| Jest + babel-jest | No | Fast, but type errors in tests go unnoticed |
| Jest + ts-jest | Yes | Noticeably slower; type-checks on every run |
| Vitest | Via your existing tsconfig | No extra transform config |
移行が見た目より安く済む理由
VitestはJest互換の expect とJest互換のモックユーティリティを意図的に実装しています。これにより移行の大部分は書き直しではなく機械的な作業になります。
describe、it、test、beforeEachなどは同じです。expect(...)のmatcherはJest互換なので、アサーションはほとんど変わりません。jest.fn()はvi.fn()、jest.spyOn()はvi.spyOn()、jest.mock()はvi.mock()になります。おおむね検索と置換です。- スナップショットは形式がJest互換なので、既存のスナップショットファイルは通常そのまま使えます。
- Vitestの設定で
globals: trueにすればdescribe/it/expectがアンビエントのまま残るので、初日にすべてのテストファイルへimportを追加する必要はありません。
速度について、正直なところ
Vitestは概して高速で、特にwatchモードではViteスタイルの依存グラフを通じて変更分だけを再実行します。コールドなマシンのCIでは差はベンチマークが示すより小さくなります。どちらも実時間の大きな割合を、アサーションではなくインストールとtransformに費やすからです。
- watchモードは差が最も明白で、開発者が毎日体感する場所です。
- どちらもCIマシン間のシャーディングに対応しているため、大規模では並列度が許す水準に両者が収束します。
ts-jestを使っているなら、計測されたJestの遅さの一部は実行ごとの型チェックであって、runnerではありません。Jest自体のせいにする前にbabel-jestと比較してください。
Jestに留まるべき場合
- コードベースがCommonJSで、今後もそのままである。移行の主な論拠が当てはまりません。
- カスタムのJest transformerや、Vitestに同等物のないJest固有のpluginに依存している。
- React Nativeを使っており、そこではJestのpresetが踏み固められた道である。
- スイートが大きく安定していて、誰も不満を言っていない。移行は実コストであり、その見返りは開発体験の改善で測られます。
移行を安全に進める
- カスタムtransformer、Jestのplugin、Jestの内部に触れるものを棚卸しします。作業量が読めないのはここだけです。
- VitestをJestと並べて追加し、
globals: trueを設定して既存のテストファイルを編集不要にします。 - 1つのディレクトリを変換します。両方のスイートをCIで走らせ、合否の数だけでなく結果そのものを差分で比較します。
- 変換したファイル全体で
jest.をvi.に検索置換し、きれいに対応しない少数を直します。 - スナップショットは意図的に再生成し、丸ごと受け入れるのではなく差分をレビューします。
- 差分が空になり、スプリント1回分まるごと新しいrunnerでスイート全体が緑になってから、Jestを外します。
The switching cost is mostly in the parts nobody lists
- Assertions and mocks usually port mechanically when the target implements a compatible API; custom transformers and framework plugins do not.
- Snapshot formats differ between runners, so plan to regenerate and review rather than port.
- Run both suites in parallel in CI for a period and diff the results. A migration that changes which tests fail is not a migration, it is a regression you have not found yet.
- Coverage numbers move on a runner change even when the tests do not, because instrumentation differs. Re-baseline any coverage gate deliberately.
The verdict
新規プロジェクトや ESM 中心: Vitest。既存の大きな Jest スイート: ESM の痛みが移行を強いない限り Jest のまま。
よくある質問
JestからVitestに切り替えるべきですか?
--experimental-vm-modules フラグを要し、jest.mock() の代わりに jest.unstable_mockModule() を使います。動作しているスイートでCommonJSなら、移行する強い理由はありません。JestはESMに対応していますか?
--experimental-vm-modules で実行し、transform: {} を設定するかtransformerからESMを出力し、.ts と .jsx には extensionsToTreatAsEsm を使う必要があります。なぜESMで jest.mock() が効かないのですか?
import 文をあなたのコードが動く前に評価するため、jest.mock() が依拠する巻き上げが成立しないからです。JestはESM向けに jest.unstable_mockModule() を提供しており、対象モジュールの動的 import() と組み合わせて使います。JestからVitestへの移行はどれくらい大変ですか?
expect とJest互換のモックを実装しているためです。jest.fn() が vi.fn() になるといった具合で、スナップショットは形式互換、globals: true により全ファイルの編集を避けられます。読めないのはカスタムのJest transformerとpluginなので、まずそれを棚卸ししてください。VitestはJestより実際に速いですか?
ts-jest を使っているなら、Jestのせいにしている一部は実行ごとの型チェックです。VitestはViteなしで動きますか?
JestはTypeScriptのテストを型チェックできますか?
ts-jest を使う場合だけです。babel-jest 経由は型チェックせずにトランスパイルすると、Jestのドキュメントが明記しており、テスト中の型エラーは黙って通ります。テストスイートが型チェックされていると思い込んでいるチームが、実はされていないと気づく落とし穴です。