Claude CodeでJestからVitestへ移行する — jest.mockと設定差の順番
JestからVitestへのテスト移行をClaude Codeで進める手順です。設定差とjest.mockの書き換えを先に片付け、ファイル単位に分けて、移行前後で同じ結果になることを確かめます。
JestからVitestへの移行は、テストファイルを1本ずつ書き換える作業の寄せ集めに見えます。実際に時間を食うのは、書き換えそのものよりも順番の取り違えです。設定の差を後回しにしてモックを直し始めると、失敗の原因がモックなのか設定なのか分からなくなります。
Claude Codeに任せるときは、先に設定、次にモック、最後にファイル単位の量産、という順に渡すと切り分けが保てます。この記事では、その順番と、各段階で「Jestのときと同じ結果になった」と言える確認の取り方を説明します。
どの順番で潰すか
結論から書くと、次の順です。
Jestからの移行を進める順番
- 1
Jestでベースラインを取る
移行前のテスト数と失敗数を、ファイル単位で記録します。
- 2
Vitestの設定を足す
vitest.config.ts、globals、setupFilesを決めます。ここではテストファイルを書き換えません。 - 3
代表ファイルでjest.mockを書き換える
モック周りの規則を1〜2ファイルで固めます。
- 4
残りをファイル単位で量産する
subagentやworktreeで分け、1ファイルごとに結果を照合します。
- 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での対処 |
|---|---|---|
| globals | Jestの挙動既定で有効 | Vitestでの対処既定は無効。globals設定で有効化するか、vitestからimportする |
__mocks__ | Jestの挙動ルートの__mocks__が自動で使われる | Vitestでの対処vi.mock()を呼ばない限り読み込まれない。全テストで使うならsetupFilesでvi.mock()する |
| フック | Jestの挙動順番に1つずつ実行 | Vitestでの対処既定の動きが違う。Jestと同じ順にするならsequence.hooksを調整する |
| タイマー | Jestの挙動旧式タイマーあり | Vitestでの対処旧式タイマーは未対応 |
| タイムアウト | Jestの挙動jest.setTimeout | Vitestでの対処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アプリを開発するで扱っています。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。