【JavaScript】Jest から Vitest 5 へ移行した記録

Jest から Vitest 5 へテスト実行環境を移行する流れを、幾何学的な図形と矢印で表したアイキャッチ画像
目次

この記事が解決する悩み

  • Jest で書いた ESM のテストが「Must use import to load ES Module」で起動しない
  • Vitest に移したいけれど、どこで何が壊れるのか先に知りたい
  • Vitest 5 に上げたとたん、モックの呼び出し回数を数えていたテストが落ちた
対象読者: Jest でテストを書いている JavaScript / フロントエンドの開発者です。前提知識として、package.json の type: module と Jest の基本的な使い方を前提にします。

結論

書き換えるのは3箇所です。
  • @jest/globals の import を vitest に変え、describe / it / expect も明示的に import する
  • jest.mock を vi.mock に変え、default export を模すときは { default: ... } の形で返す
  • jsdom を使うファイルの docblock を // @vitest-environment jsdom にする
// 移行前(Jest 30 + ESM)
import { jest } from "@jest/globals";

jest.unstable_mockModule("./greeting.js", () => ({ default: () => "モック" }));
const { default: greeting } = await import("./greeting.js");

// 移行後(Vitest 5)
import { expect, test, vi } from "vitest";
import greeting from "./greeting.js";

vi.mock("./greeting.js", () => ({ default: () => "モック" }));
Jest で必要だった await import は、静的な import に戻せます。移行後は 7 テストが 1 秒未満で通りました。

移行前の状態

題材は ESM で書いた小さな買い物カートのプロジェクトです。type: module の package.json で、テストは Jest 30.5.2 に動かしてもらっていました。 素の jest を実行すると、テストファイルの読み込みで止まります。
$ npx jest jest/tests
FAIL jest/tests/cart.test.js
  ● Test suite failed to run
    Must use import to load ES Module: 実行ディレクトリ/jest/tests/cart.test.js
      - Use Node v24.9+ where Jest supports require(esm) natively
Jest 30 が示す選択肢は、Node 24.9 以上に上げるか --experimental-vm-modules を付けるかの2つです。後者で動かすと、今度はトップレベルの jest.mock が落ちました。
$ NODE_OPTIONS=--experimental-vm-modules npx jest jest/mock-broken.test.js
  ● Test suite failed to run
    ReferenceError: require is not defined
      at require (jest/mock-broken.test.js:5:5)
      at _getJestObj (jest/mock-broken.test.js:3:1)
ESM では jest.mock が require を経由するため、モジュールの差し替えができません。回避策は jest.unstable_mockModule と await import の組み合わせです。
import { jest } from "@jest/globals";

jest.unstable_mockModule("../../src/postoffice.js", () => ({
  sendReceipt: jest.fn(async (customer) => ({ id: `mock-${customer.id}`, amount: 0 })),
}));

// モックした後に読み込む必要がある
const { subscribe } = await import("../../src/subscribe.js");

// 実行結果: Test Suites: 5 passed, 5 total / Tests: 6 passed, 6 total
テストは通りますが、「unstable」と名乗る API に構成を預ける形ですね。jsdom を使うファイルには jest-environment-jsdom パッケージの追加も要りました。

Vitest 5 に置き換えると何が壊れるか

同じファイルを Vitest 5.0.1 で実行すると、Jest のグローバル前提のコードは読み込み時点で落ちます。
$ npx vitest run jest/tests/cart.test.js
 FAIL  jest/tests/cart.test.js
ReferenceError: describe is not defined
 ❯ jest/tests/cart.test.js:3:1
Vitest の globals は既定で無効ですよ。@jest/globals を import していたファイルは、さらに手前で止まります。
$ npx vitest run jest/tests/subscribe.test.js
Error: Do not import `@jest/globals` outside of the Jest test environment
 ❯ node_modules/@jest/globals/build/index.js:23:7
jsdom は docblock 次第でした。@jest-environment jsdom は Vitest 5 でもそのまま読まれます。docblock を外すと document が無いと言われます。
$ npx vitest run broken/dom-no-docblock.test.js
ReferenceError: document is not defined
 ❯ broken/dom-no-docblock.test.js:9:3

実際に書き換えたところ

モックのファクトリの形が変わります。Jest は返り値そのものが default export でしたが、Vitest は export を並べたオブジェクトを要求します。
import { expect, test, vi } from "vitest";
import greeting from "../../src/greeting.js";

vi.mock("../../src/greeting.js", () => ({
  default: () => "こんにちは、モックさん",
}));

test("モックに差し替わる", () => {
  expect(greeting("ゆき")).toBe("こんにちは、モックさん");
});
vi.mock は静的な import より前に巻き上げられるので、await import を書く順番を気にしなくて済みます。6ファイル・7テストをまとめて流した結果です。
$ npx vitest run vitest/tests
 Test Files  1 failed | 5 passed (6)
      Tests  1 failed | 6 passed (7)
落ちた1件は書き換えミスではなく、Vitest 5 の既定値の変更が原因でした。

Vitest 5 で変わったモックの既定値

Vitest 5 から clearMocks の既定が true になりました。テストごとに vi.clearAllMocks() が呼ばれるので、モックの呼び出し履歴はテストをまたいで残りません。
import { expect, test, vi } from "vitest";

const notify = vi.fn();

test("1回目", () => {
  notify();

  expect(notify).toHaveBeenCalledTimes(1);
});

test("2回目", () => {
  notify();

  expect(notify).toHaveBeenCalledTimes(2);
});

// 実行結果
// AssertionError: expected "vi.fn()" to be called 2 times, but got 1 times
//  ❯ vitest/tests/call-history.test.js:14:18
同じテストは Jest 30 では通ります。落ちるのはモックの実装ではなく、記録された履歴のほうです。 clearMocks: false を書けば旧挙動に戻せます。
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    clearMocks: false,
  },
});

// 実行結果: Test Files  6 passed (6) / Tests  7 passed (7)

つまずきポイント

  • -t の区切り文字が空白から > に変わった。CI で -t "math adds" を渡していたテストは skipped になり、-t "math > adds" で当たる
  • Jest 側で unstable_mockModule を使っていたファイルは、Vitest に移すと vi.mock で普通に書ける(ESM でも動く)
  • jsdom は jest-environment-jsdom ではなく jsdom を入れて、environment を指定する形になる

検証環境

OS Windows 11 (build 10.0.26200)
CPU AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics
メモリ 28GB
言語/ツール Node.js 22.23.2(ホスト既存)/ jest 30.5.2 と jest-environment-jsdom 30.5.2(移行前)/ vitest 5.0.1 と vite 8.3.0 / jsdom(npm で一時ディレクトリ内に install)
使用コマンド jest jest/tests(ESM のまま実行・experimental-vm-modules フラグ あり/なし)/ vitest run(移行前のファイル・移行後のファイル・clearMocks: false の config)/ vitest run -t(区切り文字の比較)
測定日 2026-09-22
上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。
Jest 30.5.2 と Vitest 5.0.1 に同じテストを実行させた実測ログ。素の jest が ESM を読めないこと、--experimental-vm-modules では jest.mock が require is not defined で落ちること、Vitest で describe is not defined / Do not import @jest/globals / document is not defined の3つが再現すること、移行後に clearMocks の既定変更でモックの履歴テストだけが落ちて clearMocks: false で戻ること、-t の区切りが > に変わったことまでを1枚にまとめたもの
Jest 30.5.2 と Vitest 5.0.1 に同じテストを実行させた実測ログ。素の jest が ESM を読めないこと、–experimental-vm-modules では jest.mock が require is not defined で落ちること、Vitest で describe is not defined / Do not import @jest/globals / document is not defined の3つが再現すること、移行後に clearMocks の既定変更でモックの履歴テストだけが落ちて clearMocks: false で戻ること、-t の区切りが > に変わったことまでを1枚にまとめたもの

まとめ

Jest で ESM を回すために積んでいた設定は、Vitest では要りませんでした。
項目 Jest 30.5.2 Vitest 5.0.1
ESM のテスト実行 --experimental-vm-modules が必要(Node 22) そのまま動く
モジュールの差し替え unstable_mockModule + await import vi.mock(巻き上げあり)
グローバル API 既定で有効 既定で無効(明示 import)
jsdom jest-environment-jsdom を追加 jsdom を追加して environment を指定
モックの履歴 テストをまたいで残る テストごとにクリア(clearMocks が既定 true)
移行そのものは3ファイル・30分ほどの作業でした。注意すべきは clearMocks の既定変更だけで、テストの書き方そのものは Jest のままで動きますよ。

参考

本文の記述は以下の一次情報を確認して書いています。

あわせて読みたい

【広告】お名前.comならドメイン取得が格安。

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次