JestからVitest 4への完全移行ガイド — インストールから実践テストまで

JestからVitest 4への完全移行ガイド — インストールから実践テストまで

Vitest 4.1.7ベースでJestプロジェクトをステップごとに移行する実践ガイド。インストールから設定切り替え・コード変換パターン・安定Browser Mode・新マッチャー(toSatisfy, toBeOneOf)まで、実際のサンドボックスで検証した全結果をまとめた決定版の完全移行ガイド。

先月からサイドプロジェクトのテストパイプラインを整備していて、長年使ってきたJestをVitestに乗り換えた。理由はシンプルだ。TypeScriptプロジェクトでJestを維持しようとすると、ts-jestbabel-jestのような変換レイヤーが必要になる。設定項目が増えるにつれ、エラーメッセージが暗号のように読めなくなってくる。

VitestはViteと同じ変換パイプラインを使うため、TypeScriptを別設定なしでそのまま理解する。さらにVitest 4でBrowser Modeがstableに格上げされ、以前はjest-dom + JSDOMの組み合わせで模倣していたDOMテストを実際のChromiumで実行できるようになった。

以下に出てくるパターンは、すべて自分で実際に動かしたものだ。書き始める前に、サンドボックスでvitest@4.1.7を入れて16個のテストを通しておいた。だから設定ファイルを一つ一つ説明するのではなく、「Jestから来た人が詰まるポイント」を中心にまとめている。

VitestがJestより優れている点、速度ではなく設定の単純さ

「Vitestが3〜8倍速い」というベンチマーク数値をよく見かける。直接計測はしていないが、速度より自分が実感したのは設定の複雑さの違いだ。

JestでTypeScriptを使うには通常こんなパッケージが必要になる。

npm install --save-dev jest @types/jest ts-jest @jest/globals

jest.config.tsにも記述が必要だ:

export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
}

Vitestは:

npm install --save-dev vitest

これだけだ。

ただし、正直なところも言っておく。VitestはNode.jsエコシステム全体をサポートするのではなく、Viteエコシステムに最適化されている。 Next.jsやExpressのようなサーバー中心のフレームワークでJestを使っていた場合、プロジェクト規模によっては移行コストが思ったより大きくなる可能性がある。

前提条件

  • Node.js 18以上(22推奨)
  • 既存のJestプロジェクト(Jest 27〜30すべて対象)
  • TypeScript使用プロジェクトを前提に説明(JSプロジェクトも同じ流れ)

確認:

node --version  # v22.22.0
npm --version   # 10.9.4

Step 1: Vitest 4のインストール

既存のJest依存関係を先に削除する。

npm uninstall jest @types/jest ts-jest babel-jest @jest/globals jest-environment-jsdom

Vitest 4のインストール:

npm install --save-dev vitest@4

UIダッシュボードが必要な場合:

npm install --save-dev @vitest/ui

インストール確認:

npx vitest --version
# vitest/4.1.7 darwin-arm64 node-v22.22.0

52パッケージが8秒でインストールされた。Jest + ts-jestの組み合わせと比べると、パッケージ数が半分以下だ。

Step 2: vitest.config.tsの作成

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,          // describe/test/expectをimportなしで使用(Jest互換)
    environment: 'node',    // 'jsdom' | 'happy-dom' | 'browser'から選択可能
    include: ['**/*.{test,spec}.{ts,js}'],
    reporters: ['verbose'],
    coverage: {
      provider: 'v8',       // Jest: 'babel'の代わりにv8ベース
      include: ['src/**'],
      exclude: ['**/*.test.ts'],
    },
  },
})

globals: trueが重要だ。 このオプションをオンにすると、既存のJestコードからimport { describe, test, expect } from '@jest/globals'の行を削除しなくても動作する。移行初期にコードを一度に変えなくてもよいということだ。

package.jsonにスクリプトを追加:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

Step 3: Jest → Vitest コード変換パターン

ほとんどのテストコードはそのまま動く。 globals: trueを設定したため、describetestexpectbeforeEachafterEachは変換不要だ。

変換が必要なパターン:

jest.fn() → vi.fn()

// Before (Jest)
const mockFn = jest.fn((x: number) => x * 2)

// After (Vitest)
import { vi } from 'vitest'
const mockFn = vi.fn((x: number) => x * 2)

実際にサンドボックスで検証した結果、vi.fn()の動作はjest.fn()と同一だった。

jest.mock() → vi.mock()

// Before (Jest)
jest.mock('./api-service')

// After (Vitest)
vi.mock('./api-service', () => ({
  fetchUser: vi.fn(),
  createUser: vi.fn(),
}))

Vitestではvi.mock()にホイスティングが適用される。Jestと同様にファイルの先頭で動作する。

jest.requireActual() → vi.importActual()

これが最も混乱するポイントだ。一部だけモックするJestのパターン:

// Before (Jest)
jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  formatDate: jest.fn(),
}))

// After (Vitest)
vi.mock('./utils', async () => ({
  ...(await vi.importActual('./utils')),
  formatDate: vi.fn(),
}))

vi.importActual()非同期だ。async/awaitを忘れるとエラーになる。最初にここで詰まる人が多い。

vi.spyOn()

const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
console.log('test')
expect(spy).toHaveBeenCalledWith('test')
spy.mockRestore()

Step 4: Vitest 3〜4で追加された新しいマッチャー

toHaveBeenCalledExactlyOnceWith

const fn = vi.fn()
fn('hello')
expect(fn).toHaveBeenCalledExactlyOnceWith('hello')

toSatisfy

expect(42).toSatisfy((n: number) => n > 0 && n < 100)
expect('vitest').toSatisfy((s: string) => s.startsWith('vi'))

toBeOneOf

const env = process.env.NODE_ENV
expect(env).toBeOneOf(['development', 'staging', 'production'])

サンドボックスでの検証結果:

✓ toHaveBeenCalledExactlyOnceWith 0ms
✓ toSatisfy 0ms
✓ toBeOneOf 0ms

Step 5: 行番号で特定テストのみ実行

Vitest 3で追加されたが、最もよく使う機能が行番号フィルタリングだ。

npx vitest run "src/vitest4-features.test.ts:19"

実行結果:

↓ src/vitest4-features.test.ts:6  > tracks calls with vi.fn()     [skipped]
✓ src/vitest4-features.test.ts:19 > toHaveBeenCalledExactlyOnceWith  1ms
...
Tests  1 passed | 7 skipped (8)
Duration  106ms

19行目のテスト1つだけが実行され、残りはskipされた。IDE上で「この行に移動して実行」が実現できる。

Step 6: Inline Workspace (Vitest 3+)

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    workspace: [
      {
        test: {
          name: 'unit',
          environment: 'node',
          include: ['src/**/*.unit.test.ts'],
        },
      },
      {
        test: {
          name: 'integration',
          environment: 'node',
          include: ['src/**/*.integration.test.ts'],
          globalSetup: './test/setup.ts',
        },
      },
    ],
  },
})

Step 7: CI設定 (GitHub Actions)

name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm test

移行でよく詰まるポイント

1. globals: trueなしでdescribe is not definedエラー

vitest.config.tsglobals: trueを入れないと、既存のJestコードでdescribetestexpectが見つからないエラーが発生する。

2. vi.importActual()を同期で使うと空オブジェクトを返す

// 誤った例
vi.mock('./utils', () => ({
  ...vi.importActual('./utils'),  // asyncじゃない → Promiseオブジェクトが入る
}))

// 正しい例
vi.mock('./utils', async () => ({
  ...(await vi.importActual('./utils')),
}))

3. moduleNameMapperの置き換え

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  test: { globals: true },
})

サンドボックス検証結果

 RUN  v4.1.7

 ✓ src/math.jest-style.test.ts  (6 tests)   → Jestスタイルのコードをそのまま実行
 ✓ src/api-service.test.ts      (2 tests)   → vi.mock()パターン
 ✓ src/vitest4-features.test.ts (8 tests)   → 新マッチャー、vi.fn、vi.spyOn

 Test Files  3 passed (3)
      Tests  16 passed (16)
   Duration  157ms

いつ移行し、いつ見送るか

インストールコマンドを打つ前に、この乗り換えが今の自分のプロジェクトに合うかを先に判断したい。移行はタダではない。コード変換の時間、CIの再検証、チームの学習コストがかかる。以下の基準で分けてみた。

Vitestに移す価値がある場合

  • プロジェクトが既にVite、SvelteKit、Nuxt、AstroのようなViteベースのビルドを使っている。この場合、テスト変換パイプラインとビルドパイプラインが一本化され、設定の重複が消える。
  • TypeScriptを使っていて、ts-jestbabel-jestでモジュール解決エラーやESM/CJSの競合を繰り返しデバッグしている。
  • ESM(import/export)中心のコードベースだ。JestのESMサポートは依然として実験的フラグが必要だが、VitestはESMがデフォルトだ。
  • コンポーネントテストをJSDOMのシミュレーションではなく実ブラウザで走らせたい。Vitest 4のBrowser Modeがこのシナリオをstableで支えている。

移行を見送る、または避けるべき場合

  • Next.jsやExpressベースの大規模サーバーテストスイートだ。VitestはVite生態系に最適化されているため、Node.jsモジュールシステムの複雑なケースで予想外の挙動が出ることがある。公式のマイグレーションガイドmockResetの挙動の違いなど、Jestとの非互換点を明記している。
  • チームがJestのスナップショット、カスタムリゾルバ、膨大なjest.config資産に深く依存していて、すぐに書き直す余裕がない。
  • 純粋なNode.jsライブラリで、Browser Modeが不要だ。この場合Jestを維持しても損は小さく、移行の限界利益も小さい。
  • 締め切りが目前だ。移行は安定したスプリントでやるのがいい。テスト基盤を変えながら機能まで作ると、変数が二つ混ざってデバッグが難しくなる。

判断がつかなければ、小さなテストファイルを一つだけVitestに移して並行実行してみるのを勧める。globals: trueだけ入れておけば大半はそのまま通るので、30分ほどで実際の互換性を確認できる。

で、移行する価値はあるのか

私の判断はTypeScriptプロジェクトならYES、そうでなければケースバイケースだ。

TypeScriptを使うViteベースのプロジェクトでJestを維持するのは、徐々に逆方向になってきている。変換レイヤーの設定、tsconfigの競合、そこにモジュール解決のエラーまで。こうした問題をデバッグし続ける時間が勿体ない。

一方、Next.jsやExpressベースの大規模サーバーテストスイートなら慎重に判断すべきだ。VitestのVite優先設計は、複雑なサーバーサイドのモジュール解決で思わぬ挙動を見せることがある。

npm週次ダウンロード数は4.8Mから7.7Mに増えた。それだけ多くのプロジェクトが乗り換えたわけだが、全員がすんなり移行できたわけではないだろう。

BunでTypeScriptスクリプトを自動化する構成と組み合わせて、VitestをBunで走らせる試みも今やっている最中だ。これは次の記事で書く予定だ。今は4.xで移行しておくのが無難な選択だと思う。

TypeScriptのツールチェーンをさらに磨きたいなら、TypeScript SDKでMCPサーバーを段階的に構築する記事Honoで型安全なAPIを作る記事も同じ流れで併せて読んでおくといい。テスト、ランタイム、APIレイヤーをすべてVite生態系に揃えると、設定ファイルが目に見えて減る。

参考資料(一次ソース)

この記事の検証に使った公式ドキュメントだ。バージョンごとに挙動が変わることが多いので、実際の移行前には必ず原文を確認してほしい。

よくある質問

JestをVitest 4に移行すべきですか?
TypeScriptとViteベースのプロジェクトなら推奨します。ts-jestなどの変換レイヤー設定、tsconfigの競合、モジュール解決エラーをデバッグし続けるコストが大きいためです。ただしNext.jsやExpressベースの大規模サーバーテストスイートでは、VitestがViteエコシステムに最適化されている分、慎重に判断すべきです。
globals: trueオプションはなぜ重要ですか?
このオプションをオンにすると、既存のJestコードのdescribe、test、expectをimportなしでそのまま使えます。移行初期にすべてのテストファイルを一度に変える必要がなくなります。設定しないとdescribe is not definedエラーが発生します。
vi.importActual()を使うときによくある間違いは何ですか?
jest.requireActual()と違いvi.importActual()は非同期なので、mockファクトリをasyncにしてawaitを付ける必要があります。awaitを忘れるとモジュールのexportではなくPromiseオブジェクトが展開されて入り、誤動作します。最初にここで詰まる人が多いポイントです。
Vitest 4で追加されたBrowser Modeとは何ですか?
Vitest 4でBrowser Modeがexperimentalからstableに格上げされました。以前jest-dom + JSDOMの組み合わせで模倣していたDOMテストを、実際のChromiumで実行できるようになっています。

他の言語で読む

この記事は役に立ちましたか?

より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください。

著者について

jw

Kim Jangwook

AI/LLM専門フルスタック開発者

10年以上のWeb開発経験を活かし、AIエージェントシステム、LLMアプリケーション、自動化ソリューションを構築しています。Claude Code、MCP、RAGシステムの実践的な知見を共有します。

ブログリストへ