Claude Media
Claude CodeでJestからVitestへ移行する — jest.mockと設定差の順番

Claude CodeでJestからVitestへ移行する — jest.mockと設定差の順番

JestからVitestへのテスト移行をClaude Codeで進める手順です。設定差とjest.mockの書き換えを先に片付け、ファイル単位に分けて、移行前後で同じ結果になることを確かめます。

JestからVitestへの移行は、テストファイルを1本ずつ書き換える作業の寄せ集めに見えます。実際に時間を食うのは、書き換えそのものよりも順番の取り違えです。設定の差を後回しにしてモックを直し始めると、失敗の原因がモックなのか設定なのか分からなくなります。

Claude Codeに任せるときは、先に設定、次にモック、最後にファイル単位の量産、という順に渡すと切り分けが保てます。この記事では、その順番と、各段階で「Jestのときと同じ結果になった」と言える確認の取り方を説明します。

どの順番で潰すか

結論から書くと、次の順です。

手順

Jestからの移行を進める順番

  1. 1

    Jestでベースラインを取る

    移行前のテスト数と失敗数を、ファイル単位で記録します。

  2. 2

    Vitestの設定を足す

    vitest.config.ts、globals、setupFilesを決めます。ここではテストファイルを書き換えません。

  3. 3

    代表ファイルでjest.mockを書き換える

    モック周りの規則を1〜2ファイルで固めます。

  4. 4

    残りをファイル単位で量産する

    subagentやworktreeで分け、1ファイルごとに結果を照合します。

  5. 5

    Jestを外す

    全ファイルが揃ってから、依存と設定を削除します。

設定を先にする理由は、Vitestの移行ガイドが挙げる差のうち、テストファイルの外側で決まるものが多いからです。globalsを有効にするか、__mocks__をどう読み込むか、フックを並列に動かすか。これらを固定しないまま20ファイルを書き換えると、同じ失敗が20ファイルに散らばります。

着手前に整える:CLAUDE.mdとpermissions

移行中だけ有効なルールは、CLAUDE.mdに専用の節として書いておきます。Claudeが毎回同じ前提で動けるようになり、ファイルごとの指示が短く済みます。

## Jest→Vitest移行(作業中のみ)
 
- 移行前の検証は `npx jest <file>`、移行後は `npx vitest run <file>`
- globalsは有効にしない。`vi` などは `vitest` からimportする
- `jest.mock` は `vi.mock` に置き換える。ファクトリは export を明示した
  オブジェクトを返す
- 1ファイル終えるたびに、テスト数と失敗数を `migration-log.md` に追記する
- テストの期待値は変えない。落ちたら書き換えず報告する

最後の行が要です。移行で「テストが通るように期待値を書き換える」事故を、指示の段階で止めておきます。

permissionsには、移行に必要なコマンドだけを許可します。

{
  "permissions": {
    "allow": [
      "Bash(npx jest *)",
      "Bash(npx vitest run *)",
      "Bash(git diff *)",
      "Bash(git commit *)"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

npx vitestだけを許可すると、環境によってはwatchモードで起動して終了しないことがあります。許可するコマンドはrunまで書いた形にしておくと、挙動が環境に左右されません。watchの回避はClaude CodeでVitestを回すにまとめています。

手順1:Jestでベースラインを取る

書き換える前に、Jestのままで全ファイルの結果を記録します。「移行後に同じ結果」と言うための基準になります。

npx jest --verbose 2>&1 | tee jest-baseline.txt

すでに落ちているテストがあれば、その一覧も残します。ベースラインで赤いテストを移行後に「直った」と喜ぶと、移行の成否が見えなくなります。移行中は、赤は赤のまま持ち越すのが原則です。

Claudeに頼むなら、次のように範囲を絞って依頼します。

jest-baseline.txt を読んで、ファイルごとの合格数・失敗数・スキップ数を
表にして migration-log.md に書いて。テストファイルは触らないで

手順2:Vitestの設定を決める

Vitestの移行ガイドにある「Migrating from Jest」の節は、設定で決める差と、テストファイル側で直す差の両方を挙げています。この節はVitest 3と4のガイドに載っており、最新の5.0向けページでは4.0からの変更点に置き換わっています。Jestからの差は3・4の記述が基準になります。

設定側で先に決めておく項目は次のとおりです。

項目Jestの挙動Vitestでの対処
globalsJestの挙動既定で有効Vitestでの対処既定は無効。globals設定で有効化するか、vitestからimportする
__mocks__Jestの挙動ルートの__mocks__が自動で使われるVitestでの対処vi.mock()を呼ばない限り読み込まれない。全テストで使うならsetupFilesでvi.mock()する
フックJestの挙動順番に1つずつ実行Vitestでの対処既定の動きが違う。Jestと同じ順にするならsequence.hooksを調整する
タイマーJestの挙動旧式タイマーありVitestでの対処旧式タイマーは未対応
タイムアウトJestの挙動jest.setTimeoutVitestでの対処vi.setConfig({ testTimeout })

globalsは、有効にするか迷う項目です。有効にすればテストファイルはdescribeやexpectのimportなしで動きます。ただし、ガイドは「globalsを無効のままにすると、testing-libraryのような一般的なライブラリが自動のDOMクリーンアップを行わない」と注意しています。testing-libraryを使う環境では、無効にするならDOMのクリーンアップを自分で呼ぶ作業が増えます。

どちらを選ぶかは、書き換える量と戻しやすさで決まります。globalsを有効にしておけば、テストファイルの書き換えはjest.をvi.に直す範囲で済みます。無効にするなら全ファイルにimportを足す変更が加わります。大きなリポジトリでは、まず有効にして移行を終え、あとで無効化する二段階にする選択もあります。

設定の例を挙げます。内容は公式ガイドの項目に沿った形で、プロジェクトに合わせて調整してください。

import { defineConfig } from 'vitest/config'
 
export default defineConfig({
  test: {
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    sequence: { hooks: 'list' },
  },
})

sequence.hooks: 'list'は、フックの実行をJestに合わせるための設定です。ガイドは、Vitestの既定の動きがJestと違うと書き、Jestの挙動に合わせる場合にこの設定を使うと案内しています。

この段階では、まだテストファイルを書き換えません。npx vitest runを未変換のテストに向けると、jestを参照する箇所が落ちます。この失敗の種類を見て、設定側の問題か、書き換えが要る問題かを仕分けます。仕分けをClaudeに頼むときは、次の形が使えます。

npx vitest run を実行して、失敗をエラー文言ごとに分類して。
jest.* が原因のものと、設定が原因のものを分けて、ファイルは編集しないで

手順3:jest.mockを書き換える

モックの書き換えが、移行で最も判断を要する部分です。機械的に置換できるものと、挙動が変わるものがあります。

機械置換で済むもの

jest.fnはvi.fn、jest.spyOnはvi.spyOnに変わります。型の参照も、jest.MockがvitestからimportしたMock型になります。

// 移行前
let fn: jest.Mock<(name: string) => number>
 
// 移行後
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number>

jest.requireActualはawait vi.importActualに変わります。同期から非同期に変わる点に注意が必要です。

// 移行前
const { cloneDeep } = jest.requireActual('lodash/cloneDeep')
 
// 移行後
const { cloneDeep } = await vi.importActual('lodash/cloneDeep')

挙動が変わるもの

ファクトリの戻り値が違います。Jestではファクトリの戻り値がdefault exportになりますが、Vitestではexportを明示したオブジェクトを返す必要があります。

// 移行前
jest.mock('./some-path', () => 'hello')
 
// 移行後
vi.mock('./some-path', () => ({
  default: 'hello',
}))

この書き換えを見落とすと、テストが「モックされた値が取れない」という形で落ちます。原因がモックの形にあると分からないまま、期待値のほうを疑いやすい箇所です。

もう1つはmockResetです。JestのmockResetは実装を空の関数に置き換え、undefinedを返すようにします。VitestのmockResetは、モックを元の実装に戻します。vi.fn(impl)で作ったモックなら、リセットするとimplに戻ります。beforeEachでmockResetを呼んでいるテストでは、Jestで空になっていた実装が、Vitestでは元の実装に戻ります。

vi.mockは、ファイルの先頭へ巻き上げられます。変数をファクトリの中で使う場合は、vi.hoistedで変数を宣言するか、巻き上げられないvi.doMockを使います。

import { namedExport } from './path/to/module.js'
 
const mocks = vi.hoisted(() => {
  return { namedExport: vi.fn() }
})
 
vi.mock('./path/to/module.js', () => {
  return { namedExport: mocks.namedExport }
})

巻き上げの仕組みにも注意が要ります。Vitestはファイルを静的に解析して巻き上げます。公式のAPIページは、vitestから直接importしていないvi(別のユーティリティファイル経由のものなど)では巻き上げが効かないと書いています。モック用のヘルパーを別ファイルに切り出しているリポジトリでは、ここで引っかかります。

Claudeに渡す規則にする

上の差を、1ファイルずつ指示するのではなく、規則として最初に渡します。

tests/user.test.ts を Jest から Vitest に書き換えて。規則:
- jest.fn/spyOn/mock/requireActual は vi.* に置き換える
- jest.mock のファクトリが関数や文字列を直接返しているなら、
  default を明示したオブジェクトにする
- beforeEach で mockReset を呼んでいる箇所は、Jestでは実装が空になる
  動きだった。そのまま置換せず、箇所を列挙して報告する
- 期待値(expectの引数)は変えない

三つ目の規則が、判断を要する項目を黙って置換させないための歯止めです。報告された箇所だけを人が見れば済むので、全差分を読む量が減ります。

手順4:ファイル単位に分けて量産する

モックの規則が1〜2ファイルで固まったら、残りを分けます。分け方は3つあります。

方法向く場面注意点
1セッションで順に処理向く場面10ファイル前後まで注意点差分がコンテキストに積み上がる
subagentに分ける向く場面出力が多い作業を隔離したいとき注意点結果が主会話に戻る
別セッション(--worktree)向く場面数十ファイル以上注意点ブランチを後で統合する手間がある

公式のワークフロー集は、並列セッションをclaude --worktree <名前>で始める方法を示しています。別名で2つ目を起動すれば、互いの編集が衝突しない独立したセッションになります。

subagentを常に隔離して動かすなら、frontmatterにisolation: worktreeを書いた定義を置きます。移行用の例を挙げます。

---
name: test-migrator
description: JestのテストファイルをVitestに移行して結果を照合する
tools: Read, Edit, Bash
isolation: worktree
---
 
渡された1ファイルだけをJestからVitestへ書き換える。
書き換える前に `npx jest <file>` の合格数・失敗数を控え、
書き換えた後に `npx vitest run <file>` の数と照合して報告する。
期待値は変更しない。数が一致しなければ、変更を残さず原因を報告する。

worktreeで見落としやすい2点

公式のworktree解説には、この移行で効く記述が2つあります。

1つ目は、分岐元です。subagentのworktreeは、既定でリポジトリの既定ブランチ(通常はmain)から作られます。手順2で作ったvitest.config.tsをまだmainに入れていない機能ブランチ上で作業していると、subagentのworktreeには設定がありません。すべてのsubagentが「Vitestの設定がない」ことで落ちます。現在のHEADから分岐させるには、設定でworktree.baseRefを"head"にします。

{
  "worktree": {
    "baseRef": "head"
  }
}

2つ目は、CLAUDE.mdです。解説によれば、既定の場所(.claude/worktrees/の下)にあるworktreeで動くsubagentは、そのworktreeの根にあるCLAUDE.mdを読み込みません。起動時に読む指示ファイルは主会話のものです。worktree内で書き換えたCLAUDE.mdは、そのsubagentには届きません。ルールはsubagentの定義本文に直接書くか、主会話のCLAUDE.mdに置いておくのが確実です。

もう1点、同時に動かせるsubagentの数にも上限があります。既定では20個が動いている状態で、さらに起動しようとするとConcurrent subagent limit reachedで失敗します。30ファイルをいっぺんに投げると、後半の10ファイルは起動しません。CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTSで変更できますが、各subagentがトークンを使い、結果が主会話に戻る点は変わりません。10前後ずつのまとまりで回すほうが、結果の確認も追いやすくなります。

非対話モードで回す場合は、1ファイルにつき1回のclaude -pを呼びます。

while read -r f; do
  claude -p "tests/$f を規則どおりVitestへ移行して。結果を報告" \
    --allowedTools "Read,Edit,Bash(npx jest *),Bash(npx vitest run *)"
done < migrate-list.txt

注意点があります。--bareを付けて起動すると、CLAUDE.mdを含む自動の読み込みがすべて省かれます。移行ルールをCLAUDE.mdに書いている場合は、--bareなしで回すか、ルールをプロンプトに含めます。

両ランナーで結果が同じことを確かめる

書き換えたファイルはVitest専用の構文になるので、同じファイルをJestでは動かせません。そのため「両ランナーで同じ結果」は、移行前のファイルをJestで、移行後のファイルをVitestで走らせて数を照合する形になります。

照合する項目は3つです。

  • 合格数・失敗数・スキップ数がファイルごとに一致する
  • テスト名の一覧が一致する(区切りの違いを除く)
  • 移行前に赤かったテストが、移行後も同じ理由で赤い

テスト名の区切りは、Jestが空白、Vitestが>です。公式の移行ガイドも、現在のテスト名の組み立てがdescribeとtestを>で結ぶ点を差として挙げています。名前を突き合わせる際は、区切りを揃えてから比べます。

数が一致したのに、モックが効いていないケースが残ります。たとえばvi.mockの対象パスを書き間違え、本物のモジュールが呼ばれても、期待値が偶然合って通ることがあります。これを見つける確認として、テスト対象の実装を意図的に壊して、移行後のテストが落ちることを見る方法があります。公式に定められた手順ではなく、進め方の一案です。

src/user.ts の返り値を1か所だけ壊して npx vitest run tests/user.test.ts を
実行し、落ちることを確認して。確認できたら git checkout で戻して

壊したのにテストが通るなら、そのテストはモックで実装を素通りしています。移行前のJestでも同じ状態だったのか、移行で生まれたのかを、ベースラインと見比べて切り分けます。

Vitest 5へ上げるときに効く差

最新のVitest 5.0向け移行ガイドは、Node.js 22.12以上とVite 6.4以上を前提にしています。Jestから直接5.0へ移るなら、次の変更がモックの書き換えに関係します。

  • clearMocksが既定で有効になり、各テストの前にvi.clearAllMocks()が呼ばれる
  • vi.mock、vi.unmock、vi.hoistedをトップレベル以外で呼ぶとエラーになる
  • クラスのモックが実装クラスのプロトタイプを引き継ぐ

clearMocksの既定値の変更では、セットアップファイルやbeforeAllの中で呼び出しを記録するテストは、記録が各テストの前に消えます。呼び出し回数を数えるテストが、移行後に「0回」で落ちたときは、この設定を疑います。

vi.mockがdescribeの中に書かれているテストは、5.0では警告ではなくエラーになります。Jestでは通っていたテストでも、巻き上げ後の位置に移す書き換えが必要です。エラーは問題の呼び出しと位置をまとめて示すので、Claudeに「このエラーが出た箇所を、トップレベルに移して」と渡せば直せます。vi.doMockとvi.doUnmockは巻き上げられないので、どこに書いてもかまいません。

移行を2段階に分ける選択肢もあります。まず4系へ移ってJestとの差を片付け、そのあと5.0へ上げる進め方です。差の原因が「Jestとの差」と「5.0の変更」のどちらにあるのかを分けて追えます。

つまずきやすい点

requireで読み込んだモジュールがモックされない。 vi.mockが効くのはimportで読み込んだモジュールだけです。requireで読み込んでいるコードは対象外になります。

セットアップファイルで読み込んだモジュールがモックされない。setupFilesで先にimportされたモジュールは、テストファイルが走る時点でキャッシュ済みです。vi.hoistedの中でvi.resetModules()を呼ぶとキャッシュを消せます。

フックの戻り値でエラーになる。Vitestでは、beforeEachが関数を返すとそれが後片付けの関数として扱われます。矢印関数の1行書きでJestのときに値を返していたフックは、波括弧で囲んで戻り値を捨てる書き換えが要ります。

コールバック形式のテストが動かない。 doneコールバックで書かれたテストは、async/awaitかPromiseに書き換えます。

テスト名で絞り込めない。 -t 'math adds'のように空白で区切って指定していたコマンドは、>区切りに変わった名前と合いません。CIのスクリプトに残っていないかも確認します。

移行を閉じる条件

全ファイルの照合が揃ったら、依存と設定を外します。jestと関連パッケージ、jest.config.*、package.jsonのtestスクリプトを外し、CLAUDE.mdの移行専用の節を削除します。節を残すと、次の作業でもJestの手順が前提として読まれます。

最後にもう一度、全体をnpx vitest runで回して、ベースラインの総数と比べます。Jestで取った合格数と、Vitestでの合格数が一致していれば、モックの書き換えと設定の差が、結果を変えずに済んだと言えます。

同じ作業の型をJestの内側で試した例はClaude CodeでJest 30へアップグレードにあります。移行後のVueプロジェクトで検証コマンドを組む例はClaude CodeでVue 3アプリを開発するで扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. vitest.dev/guide/migration.html
  2. vitest.dev/api/vi.html
  3. vitest.dev/guide/mocking.html
  4. code.claude.com/docs/en/common-workflows
  5. code.claude.com/docs/en/worktrees
  6. code.claude.com/docs/en/sub-agents
  7. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn