web-testing-vitest について
web-testing-vitest は、Vitest を使う開発者向けのテストパターンガイドです。テスト構成、モック、アサーション、設定を扱い、バージョン変更やよくある落とし穴も説明します。
主な機能
- 01テスト構成とオプションの記述
- 02関数の代替、スパイ、モジュールモック
- 03フェイクタイマーと時刻依存テスト
- 04アサーション、非同期検証、スナップショット
- 05環境、セットアップファイル、プロジェクト設定
- 06Vitest 3/4 の移行メモ
活用シーン
- 01純粋なロジックのテスト作成
- 02DOM に依存するコードのテスト
- 03モジュールモックと HTTP 境界での傍受の選択
- 04Vitest 4 の設定やモック動作の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-testing-vitestインストール後、Claude Code で/web-testing-vitestを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/anti-patterns.mdcore.mdintegration.md詳細
Vitest テストランナーパターン
クイックガイド: Vitest はアプリのビルドと同じトランスフォームパイプラインと設定を使ってテストを実行するため、エイリアス、プラグイン、TypeScript を別のツールチェーンなしで使えます。構成には
describe/it/expect、テストダブルにはvi.fn()とvi.spyOn()、モジュール全体のモックにはvi.mock()、時間に依存する処理にはvi.useFakeTimers()を使います。Vitest 4 は現在の安定版(Vite 6 以上、Node 20 以上)で、いくつかのモックのデフォルト設定が変更されました。vi.restoreAllMocks()はオートモックをリセットしなくなり、poolOptionsはなくなりました。
詳細なリソース:
- examples/core.md — テストの構成とオプション、スパイ、モジュールのモック、フェイクタイマー、アサーション、スナップショット
- examples/integration.md — セットアップファイル、ライフサイクルフック、環境、トランスポート境界でのモック
- examples/anti-patterns.md — テスト対象にすべきでないもの
- reference.md — v3/v4 の移行に関する注意事項、設定とモック API の一覧
該当する方法
- DOM を使わない純粋なロジック — デフォルトの
environment: "node"を使い、セットアップファイルは不要です。examples/core.md を参照してください。 documentまたはwindowに触れるコード —environment: "jsdom"(またはhappy-dom)とセットアップファイルを設定します。examples/integration.md を参照してください。- 1 つのリポジトリに異なる環境を使う複数のスイートがある場合 — 環境ごとに
projectsのエントリを設定すると、1 回の実行ですべてを網羅できます。reference.md を参照してください。
<critical_requirements>
Vitest のコードを書く前に
テストオプションは第 2 引数として渡します — test("name", { timeout: 10_000 }, () => {})。末尾オプション形式は v2 以降で削除されており、オプションオブジェクトと末尾のタイムアウトを併用すると、明示的に拒否されます。
テスト対象のファイルが vi.mock() ファクトリーからインポートするエクスポートは、すべて返してください。 ファクトリーはモジュール全体を置き換えるため、省略したものはインポート時に undefined になります。残りを保持するには importOriginal() を使います。また、この呼び出しはファイル内のすべてのインポートより前にホイストされる点にも注意してください。
テスト間でモックの状態をリセットします — 設定で restoreMocks: true を指定するか、afterEach で vi.resetAllMocks() を明示的に呼び出します。v4 では vi.restoreAllMocks() が対象とするのは手動のスパイだけなので、これを呼び出さない場合、自動モックされたモジュールは状態を保持します。
コードがまたぐ境界でモックします。 モジュールモックはテストをインポートグラフに結び付けるため、関数を移動するリファクタリングを行うと壊れます。HTTP をインターセプトすればインポートグラフを自由に保ち、シリアライズも検証できます。
</critical_requirements>
自動検出: Vitest、vitest.config、vi.fn、vi.mock、vi.spyOn、vi.hoisted、vi.mockObject、vi.useFakeTimers、vi.advanceTimersByTime、mockResolvedValue、mockRejectedValue、importOriginal、toMatchInlineSnapshot、expect.schemaMatching、defineConfig の test ブロック、projects、coverage provider
対象範囲:
- テストランナーの実行と設定 — 環境、セットアップファイル、
projects、カバレッジ - テストダブル:スパイ、スタブ、モジュールモック、モックタイマー、モックされたグローバル
- アサーション、非同期アサーション、スナップショット
- テストで何をカバーするか、またどのレベルでカバーするかの判断
別の場所で扱う内容:
- コンポーネントのレンダリングと結果のクエリ — テストランナーが環境を用意し、その中で何をマウントするかは、コンポーネントテストを担当するものによって決まります
- HTTP ハンドラーの定義とインターセプトサーバーの実行 — このスキルでは、テストのライフサイクルのどこで起動してリセットするかを扱います。書き方は扱いません
- 実際のブラウザーでユーザージャーニーを実行すること — 別のツールと別のフィードバックループが必要です
Vitest はプロジェクト独自の Vite 設定を読み込むため、テストではアプリケーションとまったく同じようにインポート、エイリアス、プラグインが解決されます。これが、Vitest 自体の設定がほとんど必要ない理由であり、また失敗の原因がたいていランナーではなく設定に関する問題(「このスイートはどの環境で実行されているか」)である理由です。
<decision_framework>
テストダブルの種類を選ぶ:
- すでに参照を持っている関数が協力対象の場合 →
vi.spyOn(obj, "method")を使います。元の実装は復元できます。 - 協力対象が渡される場合 →
mockReturnValue/mockResolvedValueを指定したvi.fn()を使います。モジュールの仕組みは不要です。 - 協力対象に HTTP 経由でアクセスする場合 → モジュールではなくリクエストをインターセプトします。これにより、テストでシリアライズを検証でき、リファクタリングにも耐えられます。
- 協力対象がシームのないモジュール(ファイルシステム、時計、暗号など)の場合 → モジュールには
vi.mock()、時計にはvi.useFakeTimers()、グローバルにはvi.stubGlobal()を使います。
</decision_framework>
基本的なパターン
パターン 1:テストの構造とオプション
オプションは第 2 引数として渡します。test.each を使うとループを書かずにテーブル駆動のケースを網羅でき、it.concurrent を使うと同じファイル内の兄弟テストを並列実行できます。
describe("formatCurrency", () => {
it("formats the default currency", () => {
expect(formatCurrency(1234.56)).toBe("$1,234.56");
});
it("retries a known-flaky path", { retry: 2, timeout: 10_000 }, async () => {
await expect(fetchRate()).resolves.toBeGreaterThan(0);
});
it.each([
[0, "$0.00"],
[-1, "-$1.00"],
])("formats %d as %s", (input, expected) => {
expect(formatCurrency(input)).toBe(expected);
});
});
コード全文:examples/core.md
パターン 2:スパイとスタブ
vi.fn() は単独のテストダブルを作成し、vi.spyOn() は既存のメソッドをラップして復元できるようにします。どちらも呼び出しを .mock に記録します。
const onSave = vi.fn().mockResolvedValue({ id: "1" });
await submit({ onSave });
expect(onSave).toHaveBeenCalledWith({ title: "Draft" });
const spy = vi.spyOn(clock, "now").mockReturnValue(0);
stampEvent("saved");
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
コード全文:examples/core.md
パターン 3:モジュールのモック
vi.mock() はインポートより前にホイストされるため、vi.hoisted() から値を取得しない限り、そのファクトリーからファイル本体の内容は参照できません。テストで置き換えないエクスポートを保持するには、importOriginal を使います。
const { readConfig } = vi.hoisted(() => ({ readConfig: vi.fn() }));
vi.mock("./config", async (importOriginal) => ({
...(await importOriginal<typeof import("./config")>()),
readConfig,
}));
readConfig.mockReturnValue({ locale: "en-US" });
パターン 4:フェイクタイマー
フェイクタイマーをインストールし、明示的に時間を進めた後、実タイマーに戻して、後続のテストスイートに影響しないようにします。
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("fires the debounced callback once", () => {
const onIdle = vi.fn();
watchIdle(onIdle, 300);
vi.advanceTimersByTime(300);
expect(onIdle).toHaveBeenCalledOnce();
});
完全なコード:examples/core.md
パターン 5:アサーションとスナップショット
toStrictEqual は値だけでなく形状も比較します。resolves / rejects を使うと、非同期アサーションを 1 つの式にまとめられます。インラインスナップショットを使うと、期待値をテストの隣に記述できます。
await expect(loadUser("missing")).rejects.toThrowError(/not found/);
expect(toSummary(order)).toStrictEqual({ total: 118, items: 2 });
expect(parseArgs(["--json"]).json).toMatchInlineSnapshot(`true`);
完全なコード:examples/core.md
パターン 6:設定と環境
test ブロックはプロジェクト独自の設定ファイルに記述します。environment はテストスイートが取得するグローバル変数を決定し、setupFiles は各テストファイルの前に実行されます。また、projects を使うと、1 回の実行でそれぞれ複数の設定を指定できます。
import { defineConfig } from "vitest/config"; // not "vite" — that one has no `test` key
export default defineConfig({
test: {
environment: "node",
setupFiles: ["./tests/setup.ts"],
restoreMocks: true,
coverage: { provider: "v8", include: ["src/**/*.ts"] },
},
});
完全なコード:examples/integration.md — 対応表は reference.md を参照してください
<red_flags>
注意事項
実行時に問題が発生します:
- テスト本体の後にオプションを渡す方法 —
test("x", fn, { retry: 2 })— は v2 以降削除されています。2 番目の引数に指定してください。 - ファイル本体の
constを参照するvi.mock()ファクトリー — 呼び出しは巻き上げられるため、バインディングは一時的デッドゾーンに入ります。値はvi.hoisted()から取得してください。 - テスト対象のエクスポートだけを返すファクトリー — それ以外のエクスポートはすべて undefined になります。先に
await importOriginal()を展開してください。 defaultキーを指定せずにデフォルトエクスポートを置き換える場合 — ファクトリーの戻り値の形状がモジュールの形状になるため、default: vi.fn()が必要です。- v4 の設定での
poolOptions— 削除されています。maxWorkersとisolateはトップレベルに指定します。 - v4 の設定での
coverage.all— 削除されています。レポート対象を指定するには、coverage.includeが必須です。
予期しない動作が発生します:
- v4 の
vi.restoreAllMocks()が復元するのは手動で作成したスパイのみです。自動モック化されたモジュールにはvi.resetAllMocks()が必要です。 - v4 では
vi.fn().mock.invocationCallOrderは1から始まります。以前は0から始まっていました。 vi.fn().getMockName()は"spy"ではなく"vi.fn()"を返すため、その値を記録したスナップショットに影響します。- 自動モック化されたゲッターは、元の処理を呼び出さずに
undefinedを返します。 - フェイクタイマーをインストールしたままにすると、同じワーカー内の後続ファイルに影響します。
useFakeTimers()とuseRealTimers()を必ず組み合わせてください。 - インターセプションサーバーのハンドラーはプロセス全体で共有されます。各テストの後にリセットしないと、あるテストの上書き設定が次のテストのリクエストに応答します。
</red_flags>