コンテンツへスキップ
LatchkeyLatchkey home

CI での Vitest vs Jest: 速度、ESM、移行

テストの実行時間はしばしば CI の最も長いステップです - Vitest と Jest は速度と ESM で大きく異なります。

どちらも JS/TS のテストを実行します。Vitest は Vite ネイティブで ESM ファースト、Jest は巨大なエコシステムを持つ定番の標準です。

Vitest vs Jest at a glance

JestVitest
ESM サポートぎこちない (transform)ネイティブ
速度良い多くの場合より高速 (Vite の transform)
設定成熟、多数のプリセット最小限、Vite に整合
エコシステム最大成長中、Jest 互換 API
設定vite.config.ts を再利用独立したJest設定とtransformチェーン
アサーションJest互換の expect を備えたChaiexpect
watchモードHMR風の再実行標準のwatch
カバレッジv8またはIstanbulbabel-plugin-istanbulまたはv8
ブラウザモードありなし (jsdom/happy-domによる模擬のみ)
ベンチマーク / 型テストTinybench、expect-typeなし
シャーディングありあり。v28以降の --shard

CI では

Vitest は概して高速で、Jest の "Cannot use import statement" 失敗を引き起こす ESM transform の頭痛を回避します。Jest は大規模で成熟したスイートには安全な選択肢のままです。

  • Static import statements are evaluated before your code runs, so the hoisting jest.mock() depends on cannot happen under ESM.
  • require() of an ESM file with top-level await throws ERR_REQUIRE_ASYNC_MODULE.
  • The jest object has to come from @jest/globals or import.meta.jest rather than being ambient.
  • Jest documentation notes the underlying Node APIs it uses for this are themselves experimental.
Mocking a module under ESM
# 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 させます - いずれにせよワーカー数を制限し、メモリを適正化してください。

ApproachType-checks tests?Cost
Jest + babel-jestNoFast, but type errors in tests go unnoticed
Jest + ts-jestYesNoticeably slower; type-checks on every run
VitestVia your existing tsconfigNo extra transform config

移行が見た目より安く済む理由

VitestはJest互換の expect とJest互換のモックユーティリティを意図的に実装しています。これにより移行の大部分は書き直しではなく機械的な作業になります。

  • describeittestbeforeEach などは同じです。
  • 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が踏み固められた道である。
  • スイートが大きく安定していて、誰も不満を言っていない。移行は実コストであり、その見返りは開発体験の改善で測られます。

移行を安全に進める

  1. カスタムtransformer、Jestのplugin、Jestの内部に触れるものを棚卸しします。作業量が読めないのはここだけです。
  2. VitestをJestと並べて追加し、globals: true を設定して既存のテストファイルを編集不要にします。
  3. 1つのディレクトリを変換します。両方のスイートをCIで走らせ、合否の数だけでなく結果そのものを差分で比較します。
  4. 変換したファイル全体で jest.vi. に検索置換し、きれいに対応しない少数を直します。
  5. スナップショットは意図的に再生成し、丸ごと受け入れるのではなく差分をレビューします。
  6. 差分が空になり、スプリント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に切り替えるべきですか?
コードベースがESM、あるいはViteビルド上のTypeScriptならはい。JestのESM対応はいまも実験的と文書化されており、Nodeの --experimental-vm-modules フラグを要し、jest.mock() の代わりに jest.unstable_mockModule() を使います。動作しているスイートでCommonJSなら、移行する強い理由はありません。
JestはESMに対応していますか?
実験的にです。Jestのドキュメントは、実装にバグや機能不足があり得ること、依存する基盤のNode APIもまた実験的であることを述べています。Nodeを --experimental-vm-modules で実行し、transform: {} を設定するかtransformerからESMを出力し、.ts.jsx には extensionsToTreatAsEsm を使う必要があります。
なぜESMで jest.mock() が効かないのですか?
ESMは静的な import 文をあなたのコードが動く前に評価するため、jest.mock() が依拠する巻き上げが成立しないからです。JestはESM向けに jest.unstable_mockModule() を提供しており、対象モジュールの動的 import() と組み合わせて使います。
JestからVitestへの移行はどれくらい大変ですか?
大部分は機械的です。VitestがJest互換の expect とJest互換のモックを実装しているためです。jest.fn()vi.fn() になるといった具合で、スナップショットは形式互換、globals: true により全ファイルの編集を避けられます。読めないのはカスタムのJest transformerとpluginなので、まずそれを棚卸ししてください。
VitestはJestより実際に速いですか?
概してはい。特にwatchモードで顕著で、変更分だけを再実行します。コールドなマシンのCIでは差が縮まります。どちらも実時間の多くをインストールとtransformに費やすためです。ts-jest を使っているなら、Jestのせいにしている一部は実行ごとの型チェックです。
VitestはViteなしで動きますか?
はい。VitestはViteビルドのないプロジェクトに対しても実行でき、必要な設定を自ら作ります。すでにVite設定がある場合に利点が最大になります。テストとアプリがモジュールを同一に解決するからです。
JestはTypeScriptのテストを型チェックできますか?
ts-jest を使う場合だけです。babel-jest 経由は型チェックせずにトランスパイルすると、Jestのドキュメントが明記しており、テスト中の型エラーは黙って通ります。テストスイートが型チェックされていると思い込んでいるチームが、実はされていないと気づく落とし穴です。
Jestは非推奨ですか?
いいえ。Jestはバージョン30.4で活発に保守されています。Vitestは特にViteのエコシステムで強い勢いがありますが、「みんな移っている」は技術的な理由ではなく、動いているスイートは流行のスイートより価値があります。

関連ガイド