Vitest 5に上げたらモックの呼び出し履歴が消えた!clearMocksの変更とvi.when・vitest doctorの使い方
🙌 結論から
Vitest 5.0(2026年9月3日リリース)は、上げる前に clearMocks の既定値が変わったことだけは知っておいたほうがいいです。
Laravel + Vite のフロント側のテストを 4.1 系から 5.0 系に上げたところ、真っ先に落ちたのが beforeAll で呼んだモックの回数を後のテストで確かめているところでした(・_・;)
5.0 からは各テストの前に vi.clearAllMocks() 相当が走るので、テストをまたいで呼び出し履歴を持ち越す書き方が通らなくなっています。
逆に言うと、そこを直せば移行はかなり素直でした。
そして上げたあとのご褒美が大きくて、引数ごとに返り値を変える vi.when と、速い設定を測って教えてくれる vitest doctor は、正直これだけでも上げる価値があると思いました✨
執筆時点の最新は 5.0.4(10月12日)です。
👀 Vitest 5.0で変わったことと動かす条件
まず動かす条件です。
公式の移行ガイドでは、Node.js 22.12.0 以上と Vite 6.4.0 以上が必要になっています。
Node 20 のままの CI があると、ここで止まります。
テストの書き方に影響する変更で、私が気になったのはこのあたりです。
clearMocksの既定値がtrueになった(各テストの前に呼び出し履歴を消す。実装は残る)awaitしていないresolves/rejectsがテストの失敗になった(4 系は最後に自動で待って警告だけ)vi.mock/vi.unmock/vi.hoistedを関数やdescribeの中で呼ぶとエラーになった(vi.doMockはどこでもOK)-tのテスト名が>区切りになった(-t 'math adds'は当たらず、-t 'math > adds'なら当たる)test.sequential/describe.sequentialが削除({ concurrent: false }に置き換え)- 親ディレクトリの設定ファイルを探さなくなった(サブディレクトリから動かすなら
--configを渡す)
細かいところだと、toThrow('') が「メッセージが空」ではなく「どんなメッセージでも当たる」になっています。
空のメッセージを確かめたいなら toThrow(/^$/) です。
これ、テストが落ちずにかえって緩く通ってしまう側の変更なので、気づきにくいかも・・・🤔
🔧 4.1から上げて落ちたテストと直し方
ここは同じところでつまずく人が多そうなので、症状と直し方をそのまま書いておきます。
1. beforeAll で記録した呼び出しを見ているテストが落ちる
const track = vi.fn()
beforeAll(() => {
initAnalytics(track) // ここで track が1回呼ばれる
})
test('初期化で1回だけ送る', () => {
expect(track).toHaveBeenCalledTimes(1) // 5.0では0回になって落ちる
})
原因は clearMocks: true です。
テストの直前に履歴が消えるので、beforeAll で呼んだ1回はもう残っていません。
直し方は、確かめたい呼び出しをテストの中でやることです。
test('初期化で1回だけ送る', () => {
initAnalytics(track)
expect(track).toHaveBeenCalledTimes(1)
})
数が多くてすぐ直せないときは、vitest.config.ts の test に clearMocks: false を入れれば 4 系の動きに戻せます。
私はいったん false で上げてから、落ちるテストを1本ずつ直していきました👍
2. describe の中の vi.mock がエラーになる
describe('通知', () => {
vi.mock('./mailer') // 5.0ではエラー
})
vi.mock はファイルの先頭に巻き上げられる前提なので、5.0 からはトップレベルに出す必要があります。
テストごとにモックを変えたいなら、vi.doMock に切り替えるのが早いです。
3. -t で絞り込んでいた CI のコマンドが0件になる
package.json に vitest run -t 'UserForm submit' のような書き方が残っていると、区切りが > になったので1件も当たらなくなります。
公式のおすすめは、1つの区切りだけで絞る(-t submit)か、-t 'UserForm.*submit' のようにワイルドカードを使う形です。
4. await し忘れの rejects が赤くなる
expect(fetchUser(1)).rejects.toThrow() のように await を付けていない行は、5.0 では失敗します。
これは今まで黙って通っていたテストなので、見つかってよかった側ですね(^^)
✨ vi.whenで引数ごとのモックがかなりスッキリした
5.0 で一番うれしかったのがこれです。
今までは、引数で返り値を変えたいときに mockImplementation の中で if を書いていました。
// 4系までの書き方
vi.mocked(db.findById).mockImplementation(async (id) => {
if (id === 1) return { id: 1, name: 'Ella' }
if (id === 2) return { id: 2, name: 'Gracie' }
return undefined
})
5.0 の vi.when だと、こう書けます。
const w = vi.when(db.findById, { onUnmatched: 'throw' })
.calledWith(1).thenResolve({ id: 1, name: 'Ella' })
.calledWith(2).thenResolve({ id: 2, name: 'Gracie' })
// 全部の振る舞いが使われたかも確かめられる
expect(w).toHaveBeenExhausted()
thenReturn / thenResolve / thenThrow / thenReject と、1回だけの thenReturnOnce などがそろっています。
{ onUnmatched: 'throw' } を付けておくと、想定していない引数で呼ばれたときに vi.when: no behavior defined when called with [3] のように落ちてくれます。
「想定外の引数で undefined が返って、別の場所で落ちる」があったので、これはマジで助かりました・・!
ハマりどころも1つあります。
振る舞いは先に登録したものから当たるので、expect.any(String) のような広い条件を先に書くと、あとから書いた具体的な条件まで広い方に吸われます。
具体的なものを先、広いものを最後に書く、だけ覚えておけば大丈夫です🙌
🚀 vitest doctorに速い設定を探してもらう
vitest doctor は、今のテストを別の設定で何回か流して、どれが速いかを測ってくれるコマンドです。
npx vitest doctor
試すのは pool の threads / vmThreads、isolate: false、jsdom を使っていて happy-dom が入っていれば happy-dom への切り替え、maxWorkers を減らした場合などです。
公式ドキュメントの出力例だと、こんな感じで並びます。
Results (min of 3 runs each)
baseline (pool: forks · isolate: true) 4.08s
pool: 'threads' 3.64s (-11%)
pool: 'vmThreads' 1.33s (-67%)
isolate: false 1.28s (-69%)
Recommendation: pool: 'vmThreads' (-67%)
※この数字はドキュメントの例で、私の環境の計測値ではありません。
よくできているなと思ったのは、isolate: false を試すときにファイルの順番を入れ替えて2回流すところです。
テスト同士が状態を共有していて順番しだいで落ちる場合は、おすすめではなく失敗として出してくれます。
速さだけ見て isolate: false にして、あとで謎の失敗に悩む・・・を防いでくれるわけですね(`・ω・´)
注意点は2つです。
- 何回も全部流すので、普段の何倍か時間がかかる。流すなら昼休みの前などがよさそう
- 今の設定でテストが落ちていると、測る前に止まる
あと happy-dom への切り替えは、レイアウトや画面遷移に頼るテストだと結果が変わることがあると公式にも書かれています。
おすすめが出ても、そこは一度テストを見てから採用したいところです。
💡 .vitestフォルダとTemporalのフェイクも変わった
出力まわりも変わっていて、HTML レポートや json・junit の出力、添付ファイルがプロジェクト直下の .vitest/ にまとまるようになりました。
HTML レポートは .vitest/index.html です。
json と junit のレポーターは標準出力ではなくファイルに書くのが既定になったので、CI でログから拾っていた人は stdout オプションを見直す必要があります。
.gitignore に .vitest を1行足すのも忘れずに!
# .gitignore
.vitest
もう1つ、vi.useFakeTimers() と vi.setSystemTime() が Date と一緒に Temporal もフェイクするようになりました。
Node 26 で Temporal を使い始めたコードでも、日付を固定したテストがそのまま書けます✨
逆に Temporal だけは本物のままにしたいなら、toNotFake に Temporal を入れれば外せます。
🔗 関連記事と使い分け
- JestからVitestに乗り換えた記事:Jest から入るならまずこちら。今回はその次の、Vitest 4 から 5 へのメジャーアップデートの話です
- Node.js 26のTemporal APIの記事:Temporal で書き直したコードのテストに、5.0 のフェイクタイマーがそのまま使えます
- Node.js 26のLTSに上げる前の確認の記事:Vitest 5 は Node 22.12.0 以上が必要なので、CI の Node のバージョンを上げるときに一緒に見ておくと楽です
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
👍 まとめ
Vitest 5 へのアップグレードは、clearMocks の既定値の変更さえ押さえておけば怖くないです。
いったん clearMocks: false で上げて、落ちたテストを1本ずつテストの中で完結する形に直していくのが一番安全でした。
そのうえで、vi.when で mockImplementation の if 地獄が消えて、vitest doctor で速い設定を測ってもらえるので、上げたあとの満足度はかなり高いです(^^)
もう当たり前感あるけど、テストが速いと AI エージェントにテストまで回してもらうときの待ち時間も短くなるんですよね。
まだ 4 系のままの人は、まずローカルで npm i -D vitest@5 して落ちるテストの数を見てみてください🙏
※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報は Vitest 公式サイト をご確認ください👀