tech-book-labs
言語 / 開発ツール · 最終検証 2026-05-10 · vitest 3.x · 初公開 2026-05-10

Vitest v3 で TypeScript / React / MSW のテスト基盤を組む

Vitest v3 系で unit / component / integration テストを 1 つの runner に集約する設定パターン — globals / projects 分割 / coverage / msw 併用 / vi.mock / fake timers / playwright との分業を実装メモ化。

vitest testing msw typescript

検証日: 2026-05-10

使用バージョン: vitest@3.x

対象: Jest からの移行 / 新規プロジェクトのテスト基盤を 1 ファイルで組みたい人

Vitest v3 で unit / component / integration テスト を 1 つの runner にまとめる設定パターン。globals / projects で環境分割、coverage / MSW 併用 / vi.mock / fake timers / Playwright との分業までを整理します。

なぜ Vitest を選ぶか

  • Vite と同じ設定を使える(別の transform 設定が要らない)
  • Jest 互換 API(describe / it / expect / vi.fn がほぼそのまま動く)
  • ESM / TypeScript ネイティブ(transform 設定 0)
  • watch / coverage / UI(vitest --ui) が標準同梱
  • multi-project(unit と integration を 1 runner に)

Jest は依然として有力だが、Vite ベースのアプリなら 設定の二重管理を避ける 意味で Vitest が現代的選択。

1. 最小設定

vitest.config.ts を 1 つ置くだけで vitest コマンドが動く。globals: truedescribe/it/expect を import 不要に、environment で実行環境を選ぶ:

import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    globals: true,                     // describe/it/expect を global に
    environment: "node",               // または "jsdom" / "happy-dom"
    coverage: { provider: "v8", reporter: ["text", "html", "lcov"] },
  },
});
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

vitest だけで watch、vitest run で 1 回実行(CI 向け)。

2. unit と component を multi-project で分ける

unit は node、component は jsdom 環境が必要。1 つの config に両方 書ける:

export default defineConfig({
  test: {
    projects: [
      {
        name: "unit",
        include: ["src/**/*.test.ts"],
        environment: "node",
      },
      {
        name: "component",
        include: ["src/**/*.test.tsx"],
        environment: "jsdom",
        setupFiles: ["./vitest.setup.ts"],
      },
    ],
  },
});
import "@testing-library/jest-dom/vitest";   // toBeInTheDocument 等
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";

afterEach(() => cleanup());                   // 各 test 後に DOM をクリア

vitest --project=component で片方だけ実行も可能。

lifecycle hooks の発火順

setup → describe → beforeAll → 各 it × beforeEach/afterEach → afterAll、と入れ子になる。実行順を取り違えるとリーク・teardown 漏れの原因に。

Vitest test lifecycle — setup → describe (beforeAll → 各 it × beforeEach/afterEach → afterAll) → teardown (クリックで拡大)

実用ヒント:

  • afterEach(() => cleanup()) で React Testing Library の DOM をクリア(setupFiles で 1 回書けば全 file に効く)
  • beforeAll で重い fixture(DB / mock server)を立て、afterAll で stop:beforeEach だと毎回 spin-up で遅くなる
  • vi.useFakeTimers()beforeEach で in、afterEachuseRealTimers() を必ずペアに

3. component test の典型

React Testing Library + @testing-library/user-event を使った最小の component test。render で mount、userEvent.click で操作、expect(...).toBeInTheDocument() で確認:

import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Button } from "./Button";

describe("Button", () => {
  it("クリックで onClick が呼ばれる", async () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>送信</Button>);
    await userEvent.click(screen.getByRole("button", { name: "送信" }));
    expect(onClick).toHaveBeenCalledOnce();
  });

  it("disabled 時はクリックできない", async () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick} disabled>送信</Button>);
    await userEvent.click(screen.getByRole("button", { name: "送信" }));
    expect(onClick).not.toHaveBeenCalled();
  });
});

ポイント:

  • getByRole をデフォルトに(getByText より a11y に厚い、Testing Library の最重要原則)
  • userEventfireEvent より優先(キーボードイベントの reality に近い)
  • expect(onClick).toHaveBeenCalledOnce()(jest にはない、vitest 固有の便利 matcher)

4. MSW で network をモック

実装の fetch を mock するより、HTTP レイヤーで止める ほうが現実的。

import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const server = setupServer(
  http.get("/api/users/:id", ({ params }) => {
    return HttpResponse.json({ id: params.id, name: "Alice" });
  }),
  http.post("/api/login", async ({ request }) => {
    const body = await request.json();
    if (body.email !== "ok@example.com") {
      return new HttpResponse("Invalid", { status: 401 });
    }
    return HttpResponse.json({ token: "tok_123" });
  }),
);
import { server } from "./src/test/msw";
import { afterAll, afterEach, beforeAll } from "vitest";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

各テストで一時的にレスポンスを変える時は:

import { http, HttpResponse } from "msw";
import { server } from "../test/msw";

it("401 ならログイン失敗", async () => {
  server.use(http.post("/api/login", () => new HttpResponse("nope", { status: 401 })));
  // ... assertions
});

onUnhandledRequest: "error"mock してない URL にアクセスしたら fail(漏れ検出)。

5. vi.mock(モジュール置き換え)

特定のモジュールを テスト時だけ別実装に差し替えるvi.mock の引数 1 が対象 path、2 が置換実装の factory:

// auth/index.ts を全部 mock
vi.mock("../auth", () => ({
  getCurrentUser: vi.fn().mockReturnValue({ id: "u1", role: "admin" }),
}));

// 部分 mock(他は実装を維持)
vi.mock("../utils", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../utils")>();
  return {
    ...actual,
    fetchData: vi.fn().mockResolvedValue([]),
  };
});

vi.hoisted() を使うと mock factory 内で外側変数 を使える(jest と違う癖がある):

const { mockUser } = vi.hoisted(() => ({
  mockUser: { id: "u1", name: "Alice" },
}));

vi.mock("../auth", () => ({
  getCurrentUser: () => mockUser,
}));

6. Fake Timers(非同期 / setTimeout の制御)

setTimeout / setInterval / debounce を含むコードを、時間を待たずにテストする:

import { vi } from "vitest";

beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it("debounce が 300ms 後に発火する", () => {
  const fn = vi.fn();
  const debounced = debounce(fn, 300);
  debounced();
  expect(fn).not.toHaveBeenCalled();
  vi.advanceTimersByTime(300);
  expect(fn).toHaveBeenCalled();
});

vi.advanceTimersByTime / vi.runAllTimers で時間を進める。new Date() も mock したい時vi.setSystemTime(new Date("2026-05-10"))

7. snapshot test(慎重に)

toMatchInlineSnapshot() で出力をその場で固定。差分が変わったら CI で警告される代わりに、見るべき diff が増えがちなので「変わりにくい構造化データ」に絞って使う:

expect(rendered).toMatchSnapshot();
expect(rendered).toMatchInlineSnapshot();   // ファイル生成せず、source に直書き

ポイント:

  • 「変わったら知りたい」 ものだけに使う(全 component に snapshot は anti-pattern)
  • HTML 全体より「期待される構造のキー部分」:expect(button.outerHTML).toMatchInlineSnapshot()
  • snapshot が変わる PR では 意図的かを必ず diff で確認

8. coverage 計測

V8 ベースの coverage を有効にして、reporter で出力形式を選ぶ(text は console、html は browser 表示、lcov は CI 連携用):

{
  test: {
    coverage: {
      provider: "v8",                  // istanbul より速い
      reporter: ["text", "html", "lcov"],
      include: ["src/**/*.{ts,tsx}"],
      exclude: ["**/*.test.{ts,tsx}", "**/*.d.ts"],
      thresholds: {
        lines: 80, functions: 80, branches: 70, statements: 80,
      },
    },
  }
}
  • provider: "v8" が標準推奨(speed)
  • thresholds で CI で coverage 不足を fail に
  • HTML レポートは coverage/index.html で確認

9. Playwright との分業

Vitest と Playwright は 役割分担 する:

VitestPlaywright
範囲1 component / 1 関数ユーザーフロー全体
環境jsdom / node実ブラウザ
速度数百 test / 秒1 test / 秒
networkMSW で mock実 API or stubbed server
用途80% を担うクリティカル path のみ(login / 購入 / 投稿)

Vitest で広く、Playwright で深く」が定石。Vitest の component test で 90% カバーし、Playwright は 5-10 シナリオに絞る。

つまずいたポイント

  • describe / it が見つからない:globals: true を config に書くか、import { describe, it, expect } from "vitest" を毎回
  • jsdom で window.matchMedia が undefined:setupFiles で polyfill(Object.defineProperty(window, "matchMedia", ...))
  • MSW が listen していない:setup で server.listen() を beforeAll で呼ぶ、onUnhandledRequest: "error" で漏れ検出
  • vi.mock の hoisting:test ファイルの先頭に巻き上げられる。動的な mock factory には vi.hoisted()
  • coverage が 0%:provider: "v8" 必須、source map が壊れていると 0 になる、reportertext で確認

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784297144944

JavaScriptによるはじめてのアルゴリズム入門

河西 朝雄
詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション

Kyran Dale/嶋田 健志/木下 哲也
詳細を tech-book.net で見る
tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784627857216

はじめてのWebデザイン&amp;プログラミング : HTML、CSS、JavaScript、PHPの基本

村上 祐治
詳細を tech-book.net で見る
tech-book.net /books/9784297138714

フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が学べる本

株式会社ICS 池田 泰延/西原 翼/松本 ゆき
詳細を tech-book.net で見る