Vitest のモックとは、テスト中だけ関数やモジュールを「呼び出しを記録する偽物」に置き換え、外部への通信やメール送信などを起こさずに動きを確かめる仕組みです。 道具は大きく3つで、関数を新しく作る vi.fn、既存のメソッドを監視・差し替える vi.spyOn、import するモジュールごと置き換える vi.mock を使い分けます。

この記事では Vitest 5.0.3(2026年10月時点の npm 最新版) で、3つの使い分けを表にまとめ、すべてのコードを実際に実行したログを載せています。特に、mockClear・mockReset・mockRestore と vi.clearAllMocks・resetAllMocks・restoreAllMocks の違いを1つの表で実測し、vi.mock の巻き上げで出るエラー文と、同じファイル内の関数がモックされない現象を再現して直し方まで示します。Angular や Vue のコンポーネントテストの書き方は、それぞれ「Angular テスト Vitest 入門」「Vueのテストの書き方」の記事にまとめているので、ここではモックの使い方に集中します。

vi.fn・vi.spyOn・vi.mock の使い分け早見表

やりたいこと 使うもの 書き方
コールバック・引数の関数を偽物にする vi.fn const onSave = vi.fn()
偽物の戻り値を決める vi.fn vi.fn(() => 10) / .mockReturnValue(10)
既存オブジェクトのメソッドを監視だけする vi.spyOn vi.spyOn(obj, 'method')
既存メソッドを一時的に差し替えて後で戻す vi.spyOn .mockReturnValue(x) → .mockRestore()
import しているモジュールを丸ごと置き換える vi.mock vi.mock('./notify', () => ({ sendMail: vi.fn() }))
モジュールの一部だけ置き換える vi.mock + importOriginal { ...await importOriginal(), getTax: vi.fn() }
vi.mock の中でテスト側の変数を使う vi.hoisted const { m } = vi.hoisted(() => ({ m: vi.fn() }))
モックに TypeScript の型を付ける vi.mocked vi.mocked(sendMail).mockReturnValue('x')
fetch を置き換える vi.stubGlobal / vi.spyOn(globalThis, 'fetch') vi.stubGlobal('fetch', vi.fn().mockResolvedValue(res))
setTimeout を早送りする vi.useFakeTimers vi.advanceTimersByTime(500)
「今」の日時を固定する vi.setSystemTime vi.setSystemTime(new Date('2026-10-02'))
環境変数を変える vi.stubEnv vi.stubEnv('API_BASE', 'https://...')
テストごとに元に戻す vi.restoreAllMocks など 後述の比較表を参照

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vitestモックの使い方|vi.mock・vi.fn・vi.spyOnの使い分け表」

動作確認した環境

項目 バージョン
Node.js 22.22.2
vitest 5.0.3
TypeScript 7.0.2(tsc --noEmit で型チェック)
npm init -y && npm pkg set type=module
npm i -D vitest typescript @types/node
npx vitest run --reporter=verbose   # console.log の出力も表示される

--reporter=verbose を付けると、成功したテストの console.log も表示されます。 この記事のログはすべてこの形式で取得しました。

3つの違いを一言で

flowchart LR
  A[テストしたい関数] --> B{偽物にしたいものは?}
  B -->|引数で渡す関数| C[vi.fn]
  B -->|既存オブジェクトのメソッド| D[vi.spyOn]
  B -->|import しているモジュール| E[vi.mock]
  D --> F[mockRestore で本物に戻せる]
  E --> G[ファイル全体に効く・巻き上げられる]
比べる点 vi.fn vi.spyOn vi.mock
対象 新しい関数 既存のメソッド モジュール全体
本物の処理 なし(指定した実装) 既定では本物が動く なし(ファクトリの中身)
効く範囲 渡した場所だけ そのオブジェクト そのテストファイル全体
元に戻す ― mockRestore() vi.doUnmock / ファイルを分ける
書く位置 どこでも どこでも どこに書いてもファイル先頭へ巻き上げ

vi.fn と vi.spyOn の基本(実行ログ)

// test/basics.test.ts
import { describe, it, expect, vi } from 'vitest'
import * as notify from '../src/notify'

describe('vi.fn', () => {
  it('戻り値を決めて、呼び出しを検証する', async () => {
    const getStock = vi.fn((sku: string) => 0)
    getStock.mockReturnValue(10).mockReturnValueOnce(1)
    console.log('戻り値:', getStock('A-1'), getStock('A-2'), getStock('A-3'))
    console.log('mock.calls:', JSON.stringify(getStock.mock.calls))
    console.log('mock.results:', JSON.stringify(getStock.mock.results))
    expect(getStock).toHaveBeenCalledTimes(3)
    expect(getStock).toHaveBeenNthCalledWith(2, 'A-2')
    expect(getStock).toHaveLastReturnedWith(10)

    const load = vi.fn<() => Promise<string[]>>()
    load.mockResolvedValueOnce(['ペン']).mockRejectedValueOnce(new Error('timeout'))
    await expect(load()).resolves.toEqual(['ペン'])
    await expect(load()).rejects.toThrow('timeout')
    console.log('3回目(未設定):', load())
  })
})

describe('vi.spyOn', () => {
  it('本物を残したまま監視する/差し替える', () => {
    const spy = vi.spyOn(notify, 'sendMail')
    notify.sendMail('a@example.com', 'テスト1')
    spy.mockImplementation(() => 'mocked')
    console.log('差し替え後:', notify.sendMail('b@example.com', 'テスト2'))
    spy.mockRestore()
    console.log('mockRestore後:', notify.sendMail('c@example.com', 'テスト3'))
    expect(spy).toHaveBeenCalledTimes(0) // restore で履歴も消える
  })
})
// src/notify.ts
export function sendMail(to: string, subject: string): string {
  console.log(`[本物] ${to} にメール送信: ${subject}`)
  return 'sent'
}
戻り値: 1 10 10
mock.calls: [["A-1"],["A-2"],["A-3"]]
mock.results: [{"type":"return","value":1},{"type":"return","value":10},{"type":"return","value":10}]
3回目(未設定): undefined
[本物] a@example.com にメール送信: テスト1
差し替え後: mocked
[本物] c@example.com にメール送信: テスト3
mockRestore後: sent
 ✓ test/basics.test.ts > vi.fn > 戻り値を決めて、呼び出しを検証する 10ms
 ✓ test/basics.test.ts > vi.spyOn > 本物を残したまま監視する/差し替える 1ms

Once 付きの設定が先に使われ、使い切ると通常の設定に戻ります。 何も設定していない呼び出しは undefined を返します。vi.spyOn は差し替えるまでは本物が動く([本物] が出力されている)点が vi.fn との大きな違いです。

戻り値・実装を決めるメソッド一覧

メソッド 動き 例
mockReturnValue(v) 毎回 v を返す 在庫数を固定
mockReturnValueOnce(v) 次の1回だけ v 1回目だけ別の値
mockResolvedValue(v) Promise.resolve(v) を返す API の成功
mockResolvedValueOnce(v) 次の1回だけ resolve 1回目成功
mockRejectedValue(e) Promise.reject(e) を返す API の失敗
mockRejectedValueOnce(e) 次の1回だけ reject 再試行のテスト
mockImplementation(fn) 実装を差し替える 引数で戻り値を変える
mockImplementationOnce(fn) 次の1回だけ実装 順番に違う動き
mockThrow(e) / mockThrowOnce(e) 呼ぶと例外を投げる(v4.1 以降) 同期関数のエラー
withImplementation(fn, cb) cb の間だけ実装を差し替え 一時的な変更
mockName(name) エラー表示の名前 失敗時に読みやすく

mockThrowOnce と、Promise の結果を記録する mock.settledResults も確かめました。

const f = vi.fn().mockThrowOnce(new Error('在庫なし')).mockReturnValue('ok')
expect(() => f()).toThrow('在庫なし')
console.log('2回目:', f(), '/ results:', JSON.stringify(f.mock.results.map((r) => r.type)))
const g = vi.fn().mockResolvedValueOnce('A').mockRejectedValueOnce(new Error('NG'))
await g(); await g().catch(() => {})
console.log('settledResults:', JSON.stringify(g.mock.settledResults.map((r) => r.type)))
2回目: ok / results: ["throw","return"]
settledResults: ["fulfilled","rejected"]

呼び出しを検証するマッチャー一覧

マッチャー 確かめること
toHaveBeenCalled() 1回以上呼ばれた
toHaveBeenCalledOnce() ちょうど1回
toHaveBeenCalledTimes(n) n 回
toHaveBeenCalledWith(...args) いずれかの呼び出しがこの引数
toHaveBeenLastCalledWith(...args) 最後の呼び出しの引数
toHaveBeenNthCalledWith(n, ...args) n 回目の引数(1始まり)
toHaveBeenCalledExactlyOnceWith(...args) 1回だけ、かつこの引数
toHaveReturned() / toHaveReturnedWith(v) 例外なく返った/v を返した
toHaveLastReturnedWith(v) / toHaveNthReturnedWith(n, v) 最後/n 回目の戻り値
toHaveResolved() / toHaveResolvedWith(v) Promise が resolve した/v で
expect.any(Number) / expect.objectContaining({...}) 引数の一部だけを比べる
.mock のプロパティ 中身
mock.calls 呼び出しごとの引数の配列
mock.results { type: 'return' | 'throw', value }
mock.settledResults Promise の fulfilled / rejected
mock.lastCall 最後の呼び出しの引数
mock.instances / mock.contexts new で作られたインスタンス/this

引数の一部だけ確かめたいときは expect.objectContaining を組み合わせると、日時などテストのたびに変わる値を無視できます。

mockClear・mockReset・mockRestore の違い(実測)

一番迷いやすいのがこの6つです。 vi.fn(() => '初期実装') に mockReturnValue('差し替え') を設定したものと、vi.spyOn(obj, 'hello') に同じく差し替えを設定したものを1回ずつ呼び、各メソッドを実行した後の状態を記録しました。

// test/reset.test.ts
import { it, vi } from 'vitest'

const obj = { hello: () => '本物' }

function check(label: string, action: (fn: any, spy: any) => void) {
  const fn = vi.fn(() => '初期実装')
  fn.mockReturnValue('差し替え')
  fn()
  const spy = vi.spyOn(obj, 'hello').mockReturnValue('差し替え')
  obj.hello()
  action(fn, spy)
  const row = [label, `fn: calls=${fn.mock.calls.length} 戻り値=${fn()}`, `spyOn: calls=${spy.mock.calls.length} 戻り値=${obj.hello()} 本物に戻った=${obj.hello !== spy}`]
  console.log(row.join(' | '))
  vi.restoreAllMocks(); obj.hello = () => '本物'
}

it('個別メソッドと一括メソッドの比較', () => {
  check('何もしない      ', () => {})
  check('mockClear       ', (fn, spy) => { fn.mockClear(); spy.mockClear() })
  check('mockReset       ', (fn, spy) => { fn.mockReset(); spy.mockReset() })
  check('mockRestore     ', (fn, spy) => { fn.mockRestore(); spy.mockRestore() })
  check('vi.clearAllMocks', () => vi.clearAllMocks())
  check('vi.resetAllMocks', () => vi.resetAllMocks())
  check('vi.restoreAllMocks', () => vi.restoreAllMocks())
})
何もしない       | fn: calls=1 戻り値=差し替え | spyOn: calls=1 戻り値=差し替え 本物に戻った=false
mockClear        | fn: calls=0 戻り値=差し替え | spyOn: calls=0 戻り値=差し替え 本物に戻った=false
mockReset        | fn: calls=0 戻り値=初期実装 | spyOn: calls=0 戻り値=本物 本物に戻った=false
mockRestore      | fn: calls=0 戻り値=初期実装 | spyOn: calls=0 戻り値=本物 本物に戻った=true
vi.clearAllMocks | fn: calls=0 戻り値=差し替え | spyOn: calls=0 戻り値=差し替え 本物に戻った=false
vi.resetAllMocks | fn: calls=0 戻り値=初期実装 | spyOn: calls=0 戻り値=本物 本物に戻った=false
vi.restoreAllMocks | fn: calls=1 戻り値=差し替え | spyOn: calls=1 戻り値=本物 本物に戻った=true
メソッド 呼び出し履歴 差し替えた戻り値 vi.fn(初期実装) は spyOn のメソッドは
mockClear / vi.clearAllMocks 消える 残る 差し替えのまま spy のまま
mockReset / vi.resetAllMocks 消える 消える 初期実装に戻る spy のまま(本物の処理を呼ぶ)
mockRestore 消える 消える 初期実装に戻る 本物に戻る
vi.restoreAllMocks 残る vi.fn は残る 変わらない 本物に戻る

ポイントは3つです。

  • mockReset は「何も返さない」ではなく、vi.fn(実装) に渡した最初の実装に戻ります。 spyOn では本物の処理を呼ぶ状態に戻ります。
  • vi.restoreAllMocks は vi.spyOn で作ったものを本物に戻すだけで、vi.fn や自動モックの履歴・実装には触れません(公式ドキュメントの記載どおりの結果でした)。
  • テストごとに「履歴も実装もまっさら」にしたいなら、vi.resetAllMocks() と vi.restoreAllMocks() を両方呼ぶのが確実です。
import { afterEach, vi } from 'vitest'

afterEach(() => {
  vi.resetAllMocks()    // 履歴と差し替えを消す(vi.fn は初期実装に)
  vi.restoreAllMocks()  // spyOn を本物に戻す
})

設定ファイルで毎回自動にすることもできます。各オプションの細かい挙動は公式ドキュメントで確認してください(この記事では設定ファイル版は未実行です)。

// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
  test: { clearMocks: true, mockReset: true, restoreMocks: true },
})

vi.mock でモジュールを置き換える

注文を受けるとメールを送る関数をテストします。テストしたいのは placeOrder で、sendMail は本当に送らないよう置き換えます。

// src/order.ts
import { sendMail } from './notify'

export function placeOrder(email: string, item: string) {
  const status = sendMail(email, `${item} のご注文を受け付けました`)
  return { item, status }
}

巻き上げ(hoisting)で出るエラーを再現

vi.mock は、どこに書いてもファイルの先頭(import より前)に移動して実行されます。 そのため、テストファイルで先に宣言した変数をファクトリの中で使うと、まだ初期化されていません。

// test/hoist-ng.test.ts(直す前)
import { it, expect, vi } from 'vitest'
import { placeOrder } from '../src/order'

const sendMailMock = vi.fn(() => 'mocked')
vi.mock('../src/notify', () => ({ sendMail: sendMailMock }))

it('注文するとメールが送られる', () => {
  placeOrder('a@example.com', 'ノート')
  expect(sendMailMock).toHaveBeenCalledOnce()
})
 FAIL  test/hoist-ng.test.ts [ test/hoist-ng.test.ts ]
Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock
 ❯ src/order.ts:1:1
      1| import { sendMail } from './notify'
       | ^
Caused by: ReferenceError: Cannot access 'sendMailMock' before initialization
 ❯ test/hoist-ng.test.ts:5:45

このコードは tsc の型チェックは通るため、実行して初めてエラーになります。 直し方は vi.hoisted で変数も一緒に先頭へ巻き上げることです。

// test/hoist-ok.test.ts(直した後)
import { it, expect, vi } from 'vitest'
import { placeOrder } from '../src/order'

const { sendMailMock } = vi.hoisted(() => ({ sendMailMock: vi.fn(() => 'mocked') }))
vi.mock('../src/notify', () => ({ sendMail: sendMailMock }))

it('注文するとメールが送られる', () => {
  const r = placeOrder('a@example.com', 'ノート')
  console.log(JSON.stringify(r), JSON.stringify(sendMailMock.mock.calls))
  expect(sendMailMock).toHaveBeenCalledWith('a@example.com', 'ノート のご注文を受け付けました')
})
{"item":"ノート","status":"mocked"} [["a@example.com","ノート のご注文を受け付けました"]]
 ✓ test/hoist-ok.test.ts > 注文するとメールが送られる 3ms

自動モックと vi.mocked で型を付ける

ファクトリを渡さない vi.mock(path) は自動モックで、モジュール内の関数がすべて vi.fn() になります。 import した関数は元の型のままなので、vi.mocked() で包むと mockReturnValue などが型付きで使えます。

// test/mocked.test.ts
import { it, expect, vi, beforeEach } from 'vitest'
import { placeOrder } from '../src/order'
import { sendMail } from '../src/notify'

vi.mock('../src/notify') // ファクトリなし=自動モック

beforeEach(() => { vi.mocked(sendMail).mockReturnValue('queued') })

it('vi.mocked で型付きのモックとして扱う', () => {
  const r = placeOrder('b@example.com', 'ペン')
  console.log(JSON.stringify(r), 'isMock=', vi.isMockFunction(sendMail))
  expect(vi.mocked(sendMail).mock.calls[0][0]).toBe('b@example.com')
})
{"item":"ペン","status":"queued"} isMock= true
 ✓ test/mocked.test.ts > vi.mocked で型付きのモックとして扱う 4ms

vi.mocked は型を付けるだけで、中身は渡したものをそのまま返します。 モックになっているのは vi.mock のおかげです。

同じファイル内の関数がモックされない現象

部分モック(importOriginal)で getTax を差し替えても、同じファイルの total の中から呼ばれる getTax は本物のままです。

// src/price.ts
export function getTax(price: number): number {
  return Math.floor(price * 0.1)
}
export function total(price: number): number {
  return price + getTax(price) // 同じファイル内の getTax を直接呼ぶ
}
// test/same-module.test.ts(直す前)
import { it, expect, vi } from 'vitest'
import { getTax, total } from '../src/price'

vi.mock('../src/price', async (importOriginal) => {
  const mod = await importOriginal<typeof import('../src/price')>()
  return { ...mod, getTax: vi.fn(() => 0) } // 部分モック:getTax だけ差し替え
})

it('getTax はモックされている', () => {
  console.log('getTax(1000) =', getTax(1000))
  expect(getTax(1000)).toBe(0)
})
it('total の中の getTax はモックされない', () => {
  console.log('total(1000) =', total(1000))
  expect(total(1000)).toBe(1000)
})
getTax(1000) = 0
total(1000) = 1100
 ✓ test/same-module.test.ts > getTax はモックされている 3ms
 × test/same-module.test.ts > total の中の getTax はモックされない 6ms
   → expected 1100 to be 1000 // Object.is equality

vi.mock が置き換えるのは「モジュールの外に公開された出口(export)」です。 total は同じファイルの中にある getTax を直接呼んでいるので、出口を差し替えても影響しません。vi.spyOn(price, 'getTax') でも同じで、実行結果は price.getTax(1000) = 0 / price.total(1000) = 1100 でした。

直し方は、差し替えたい関数を別ファイルに分けることです。

// src/fixed/tax.ts
export function getTax(price: number): number {
  return Math.floor(price * 0.1)
}
// src/fixed/price.ts
import { getTax } from './tax' // 別ファイルに分ける
export function total(price: number): number {
  return price + getTax(price)
}
// test/same-module-fixed.test.ts(直した後)
import { it, expect, vi } from 'vitest'
import { total } from '../src/fixed/price'

vi.mock('../src/fixed/tax', () => ({ getTax: vi.fn(() => 0) }))

it('別ファイルに分けると total の中でもモックされる', () => {
  console.log('total(1000) =', total(1000))
  expect(total(1000)).toBe(1000)
})
total(1000) = 1000
 ✓ test/same-module-fixed.test.ts > 別ファイルに分けると total の中でもモックされる 3ms
対策 方法 向いている場面
別ファイルに分ける 差し替えたい関数を別モジュールにして import 外部通信・メールなど境界にある処理
引数で受け取る(依存性の注入) total(price, getTax = defaultGetTax) テストで差し替える関数が少ない
モックせずに入力で確かめる total(1000) が 1100 になることを検証 計算だけの純粋な関数

税額計算のような純粋な関数は、そもそもモックせずに結果を確かめるのが読みやすいテストです。モックは外部通信やメール送信など「テストで本当に動かしたくないもの」に絞ると、テストが壊れにくくなります。

fetch・タイマー・日付・環境変数のモック

// src/api.ts
export type User = { id: number; name: string }
export async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`https://api.example.com/users/${id}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}
// src/util.ts
export function isExpired(deadline: string): boolean {
  return Date.now() > new Date(deadline).getTime()
}
export function debounce(fn: () => void, ms: number) {
  let t: ReturnType<typeof setTimeout> | undefined
  return () => { clearTimeout(t); t = setTimeout(fn, ms) }
}
export function apiBase(): string {
  return process.env.API_BASE ?? 'http://localhost:3000'
}
// test/env.test.ts
import { describe, it, expect, vi, afterEach } from 'vitest'
import { fetchUser } from '../src/api'
import { isExpired, debounce, apiBase } from '../src/util'

afterEach(() => {
  vi.useRealTimers()
  vi.unstubAllEnvs()
  vi.unstubAllGlobals()
  vi.restoreAllMocks()
})

describe('fetch', () => {
  it('vi.stubGlobal で fetch を差し替える', async () => {
    const fetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ id: 1, name: '佐藤' }), { status: 200, headers: { 'content-type': 'application/json' } }),
    )
    vi.stubGlobal('fetch', fetchMock)
    console.log('取得結果:', JSON.stringify(await fetchUser(1)))
    expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users/1')
  })
  it('vi.spyOn(globalThis, "fetch") でエラー応答', async () => {
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('not found', { status: 404 }))
    await expect(fetchUser(99)).rejects.toThrow('HTTP 404')
    console.log('404 のとき:', await fetchUser(99).catch((e) => e.message))
  })
})

describe('タイマーと日付', () => {
  it('vi.useFakeTimers で debounce を進める', () => {
    vi.useFakeTimers()
    const save = vi.fn()
    const debounced = debounce(save, 500)
    debounced(); debounced(); debounced()
    vi.advanceTimersByTime(499)
    console.log('499ms 後の呼び出し回数:', save.mock.calls.length)
    vi.advanceTimersByTime(1)
    console.log('500ms 後の呼び出し回数:', save.mock.calls.length)
    expect(save).toHaveBeenCalledOnce()
  })
  it('vi.setSystemTime で「今」を固定する', () => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date('2026-10-02T09:00:00+09:00'))
    console.log('new Date():', new Date().toISOString())
    console.log('期限 2026-10-01 は切れている?', isExpired('2026-10-01T23:59:59+09:00'))
    console.log('期限 2026-10-03 は切れている?', isExpired('2026-10-03T00:00:00+09:00'))
  })
})

describe('環境変数', () => {
  it('vi.stubEnv で process.env を一時的に変える', () => {
    console.log('既定:', apiBase())
    vi.stubEnv('API_BASE', 'https://staging.example.com')
    console.log('stubEnv 後:', apiBase())
    vi.unstubAllEnvs()
    console.log('unstubAllEnvs 後:', apiBase())
  })
})
取得結果: {"id":1,"name":"佐藤"}
404 のとき: HTTP 404
499ms 後の呼び出し回数: 0
500ms 後の呼び出し回数: 1
new Date(): 2026-10-02T00:00:00.000Z
期限 2026-10-01 は切れている? true
期限 2026-10-03 は切れている? false
既定: http://localhost:3000
stubEnv 後: https://staging.example.com
unstubAllEnvs 後: http://localhost:3000
 ✓ test/env.test.ts > fetch > vi.stubGlobal で fetch を差し替える 52ms
 ✓ test/env.test.ts > fetch > vi.spyOn(globalThis, "fetch") でエラー応答 2ms
 ✓ test/env.test.ts > タイマーと日付 > vi.useFakeTimers で debounce を進める 5ms
 ✓ test/env.test.ts > タイマーと日付 > vi.setSystemTime で「今」を固定する 2ms
 ✓ test/env.test.ts > 環境変数 > vi.stubEnv で process.env を一時的に変える 1ms
対象 置き換える 元に戻す 補足
fetch vi.stubGlobal('fetch', vi.fn()) vi.unstubAllGlobals() Node 18 以降は Response が標準で使える
fetch(監視しつつ) vi.spyOn(globalThis, 'fetch') vi.restoreAllMocks() 呼び出しも検証できる
setTimeout / setInterval vi.useFakeTimers() → advanceTimersByTime(ms) vi.useRealTimers() runAllTimers() で全部進める
今の日時 vi.setSystemTime(date) vi.useRealTimers() 表示は UTC(toISOString)
環境変数 vi.stubEnv(name, value) vi.unstubAllEnvs() import.meta.env も対象

debounce は 499ms では0回、500ms でちょうど1回になり、3回続けて呼んでも最後の1回だけ実行されることが確かめられました。偽のタイマーは既定で setTimeout・setInterval・setImmediate・Date などを置き換え、process.nextTick と queueMicrotask は toFake で指定したときだけ置き換えます(公式ドキュメントの記載)。

setSystemTime に +09:00 付きで渡した時刻は、toISOString() では UTC(00:00:00.000Z)で表示されます。 日付の境目を確かめるテストでは、タイムゾーンを明記して書くと読み違いを防げます。

よくあるエラーと直し方

エラー・症状 原因 直し方
Cannot access 'xxx' before initialization vi.mock のファクトリで外の変数を参照 vi.hoisted で一緒に巻き上げる
部分モックしたのに本物が動く 同じファイル内から直接呼んでいる 別ファイルに分ける/引数で受け取る
前のテストの呼び出し回数が残る 履歴をクリアしていない afterEach で vi.clearAllMocks() など
restoreAllMocks したのに vi.fn の戻り値が残る restoreAllMocks は spyOn 専用 vi.resetAllMocks() も呼ぶ
mockReset 後に undefined を期待したら初期実装が返った mockReset は vi.fn(実装) の実装に戻す 期待値を初期実装に合わせる
fetch のモックが次のテストに残る stubGlobal を戻していない vi.unstubAllGlobals()
偽タイマーのまま他のテストが止まる useRealTimers を呼んでいない afterEach(() => vi.useRealTimers())

よくある質問

Q. vi.fn と vi.spyOn はどちらを使えばよいですか? A. テスト対象に引数として渡す関数なら vi.fn、すでにあるオブジェクトのメソッドなら vi.spyOn です。spyOn は差し替えるまで本物が動くので、「呼ばれたかだけ確かめたい」ときにも使えます。

Q. vi.mock を test の中に書いても、そのテストだけに効きますか? A. 効きません。vi.mock はファイルの先頭に巻き上げられ、ファイル全体に効きます。 テストごとに戻り値を変えたいときは、vi.mock で関数を vi.fn() にしておき、各テストで vi.mocked(fn).mockReturnValue(...) を設定します。

Q. mockReset と mockRestore はどちらを使えばよいですか? A. 実測の表のとおり、vi.fn には mockReset(初期実装に戻る)、vi.spyOn で本物に戻したいときは mockRestore です。一括で戻すなら vi.resetAllMocks() と vi.restoreAllMocks() を組み合わせます。

Q. 外部 API の呼び出しはすべてモックするべきですか? A. 単体テストでは fetch や API クライアントをモックすると速く安定します。API の形が変わったことにも気づきたい場合は、モックを使わない結合テストも別に用意すると安心です。

まとめ

  • 引数の関数は vi.fn、既存メソッドは vi.spyOn、import するモジュールは vi.mock
  • vi.spyOn は差し替えるまで本物が動く。mockRestore で本物に戻る
  • mockClear は履歴だけ、mockReset は実装も(vi.fn は初期実装へ)、restoreAllMocks は spyOn だけを本物に戻す
  • vi.mock はファイル先頭に巻き上げられる。外の変数は vi.hoisted で渡す
  • 同じファイル内から呼ぶ関数はモックされない。別ファイルに分けて差し替える
  • fetch は stubGlobal、タイマーは useFakeTimers、日時は setSystemTime、環境変数は stubEnv
  • afterEach で戻す処理をまとめると、テスト同士が影響し合わない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vitestモックの使い方|vi.mock・vi.fn・vi.spyOnの使い分け表」

株式会社RJCでは、Vitest のモックを使ったテストの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?