第64回:Next.js × Vitest 実務深掘り完全ガイド(初心者にもわかる詳説版)

公開日:2026-02-24|キーワード:Next.js / App Router / Vitest / Testing Library / MSW / CI / Coverage

Vitestの紹介記事は増えてきましたが、実務で困るのは「結局どう設計して、どう回すのが正解?」という点です。 特にNext.js(App Router)では、Server Components / Route Handlers / Server Actions などが入り、テストの切り方が複雑になりがちです。

この記事は、初心者にも理解できる説明をベースにしつつ、 実務チームがそのまま導入・運用できるように 設定・サンプル・落とし穴・考え方まで深掘りします。 長文ですが、読み終えたときに「どこから手を付ければよいか」が明確になる構成にしています。

ゴール:
・Next.jsプロジェクトにVitestを入れて、迷わずテストを書ける状態にする
・App Router時代の「テストの役割分担」を理解する
・CIで自動実行し、チームで継続できる形にする

1. まず前提:Next.jsで“何をテストするか”を分ける

テストがうまく回らないチームの典型は、「全部を同じ種類のテストでやろうとしている」ことです。 Next.jsには、大きく3つのレイヤーがあります。

初心者向けの結論:
まずは「純粋ロジックの単体テスト」と「UIのコンポーネントテスト」をVitestで固める。
E2Eは後からPlaywrightで追加するのが一番失敗しにくいです。

2. なぜVitestなのか(Jestとの差を“初心者向け”に整理)

Jestは成熟したテストランナーで、巨大エコシステムがあります。 一方、Next.jsの世界はESM・高速ビルド・App Router前提へ寄っています。 そのとき、Jestで起きがちな悩みは次のようなものです。

Vitestは、Viteの高速トランスパイルやESM思想と相性が良く、 「設定で苦しむ時間」を減らしやすいのがメリットです。

Vitestは「速い」だけではなく、ESM前提の現代的なプロジェクトで“自然に動く”ことが価値。

3. Next.js + Vitest:最小構成から始める(導入手順)

3-1. 依存関係のインストール

まずはReactコンポーネントをテストする前提で、Testing Libraryとjsdom環境を入れます。

npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom

※ Next.jsがReactを持っているので、通常はreact/react-domを追加する必要はありません(すでに依存に含まれているため)。

3-2. vitest.config.ts(最初はシンプルに)

import { defineConfig } from "vitest/config"

export default defineConfig({
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: "./vitest.setup.ts",
  },
})

3-3. vitest.setup.ts(jest-domを有効化)

import "@testing-library/jest-dom"
ここまででできること:
・Reactコンポーネントの描画テスト
・クリック等のユーザー操作テスト(必要ならuser-eventを追加)
・純粋ロジックの単体テスト

4. “Next.js特有のつまずき”を先に潰す

4-1. パスエイリアス(@/)をテストでも使えるようにする

Next.jsプロジェクトでは @/ をよく使います。 テストで import ... from "@/lib/..." が解決できないと、最初に詰まります。

最も確実なのは、Vite側にaliasを設定することです。

import { defineConfig } from "vitest/config"
import path from "node:path"

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: "./vitest.setup.ts",
  },
})

※ あなたのプロジェクトで src を使っていないなら、././app 等に合わせてください。

4-2. CSSや画像importで落ちる場合(初心者がよく遭遇)

コンポーネントがCSS Modulesをimportしていると、テスト環境で失敗することがあります。 この場合は「スタイルはテストしない」方針で、スタブに逃がします。

Vitestでは、ViteプラグインでCSSを扱えることも多いですが、最初は割り切ってスタブでもOKです。

5. コンポーネントテスト(App Router想定)

5-1. 最小例:Pageコンポーネントの描画

import { render, screen } from "@testing-library/react"
import Page from "./page"

describe("Page", () => {
  it("renders heading", () => {
    render(<Page />)
    expect(screen.getByRole("heading")).toBeInTheDocument()
  })
})

ここで重要なのは、「画面の見た目そのもの」ではなく、 ユーザーが意味として認識する要素(role、text、label)で検証することです。 これがTesting Libraryの思想で、テストが壊れにくくなります。

5-2. ユーザー操作(フォームなど)をテストしたい場合

実務では、クリック・入力・送信が多いです。 その場合は @testing-library/user-event を追加します。

npm install -D @testing-library/user-event
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import LoginForm from "./LoginForm"

it("submits when valid", async () => {
  render(<LoginForm />)
  const user = userEvent.setup()

  await user.type(screen.getByLabelText("Email"), "a@b.com")
  await user.type(screen.getByLabelText("Password"), "pass1234")
  await user.click(screen.getByRole("button", { name: "Sign in" }))

  expect(screen.getByText("Signing in...")).toBeInTheDocument()
})
ポイント:“入力できるか”ではなく“ユーザーが達成したいことが達成されるか”をテスト対象にすると、価値が高いテストになります。

6. Server Componentsをどう扱うか(ここがApp Routerの核心)

App RouterではServer Componentsがデフォルトです。 ただし、Server Componentsは「サーバー側で実行される」前提なので、 jsdom環境でそのままレンダリングしようとすると無理が出ます。

結論:
Server Componentは無理にUIとしてテストしない。
代わりに、中のロジックをpure functionに分離して単体テストする。

6-1. 分離の具体例(初心者向け)

よくあるのは「DBから取得→整形→表示」です。 整形部分を lib/ に切り出すだけでも、テスト可能性が急に上がります。

// lib/formatUser.ts
export function formatUserName(first: string, last: string) {
  return `${last} ${first}`.trim()
}
// lib/formatUser.test.ts
import { describe, it, expect } from "vitest"
import { formatUserName } from "./formatUser"

describe("formatUserName", () => {
  it("formats last + first", () => {
    expect(formatUserName("Taro", "Yamada")).toBe("Yamada Taro")
  })
})

これだけでも、Server側の処理の品質を安定させられます。 UIは、Client Component側(フォーム等)を重点的にテストする戦略が取りやすいです。

7. Route Handlers(/api)テスト:Next.jsのAPIをVitestで検証する

Next.jsのRoute Handlers(app/api/**/route.ts)は、実務で重要です。 ここをテストできると、E2Eの負担を減らせます。

7-1. GETの最小例

// app/api/hello/route.ts
export async function GET() {
  return Response.json({ ok: true })
}
// app/api/hello/route.test.ts
import { GET } from "./route"

test("GET returns ok", async () => {
  const res = await GET()
  expect(res.status).toBe(200)
  const body = await res.json()
  expect(body.ok).toBe(true)
})
Edge Runtimeを使っている場合、NodeのAPIとの差が出ることがあります。
その場合は「どの実行環境を本番にするか」を最初に決めて、テストも揃えるのが安全です。

8. ESMモック戦略:Next.jsで“詰まる人”が多いポイント

VitestはESM前提なので、モックの考え方が「静的importをどう差し替えるか」になります。 ここを理解すると、急に扱いやすくなります。

8-1. 基本:vi.mock

import { vi } from "vitest"

vi.mock("@/lib/db", () => ({
  getUser: vi.fn().mockResolvedValue({ id: 1, name: "Test" }),
}))

初心者がやりがちなミスは、モックを書く位置やimport順序がバラバラになることです。 「モック→対象import」の順に統一すると事故が減ります。

8-2. Next.js特有:next/navigation をモックする

App Routerでは useRouterredirect を使います。 これらはテストでそのまま動かないことが多いので、モックします。

vi.mock("next/navigation", () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    prefetch: vi.fn(),
  }),
}))

※ 実際に使っているAPI(pushだけなど)に合わせて最小化してください。

9. 通信を含むUIテスト:MSWで“本物っぽく”する

フォーム送信や一覧取得のUIをテストする場合、 単純にAPI関数をモックするだけだと「実際の通信フロー」が検証できません。

そこで有効なのが MSW(Mock Service Worker) です。 テスト中だけ “擬似サーバー” を立てて、fetchリクエストに応答させられます。

9-1. インストール

npm install -D msw

9-2. 例:テスト用のハンドラ

// test/msw/handlers.ts
import { http, HttpResponse } from "msw"

export const handlers = [
  http.get("/api/users", () => {
    return HttpResponse.json([{ id: 1, name: "Alice" }])
  }),
]

9-3. セットアップ

// test/msw/server.ts
import { setupServer } from "msw/node"
import { handlers } from "./handlers"

export const server = setupServer(...handlers)
// vitest.setup.ts
import "@testing-library/jest-dom"
import { server } from "./test/msw/server"

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
MSWを入れると「ネットワークが絡むUI」でも、E2Eをやらずにかなりの品質を担保できます。
初心者チームほど、まずはMSW + コンポーネントテストを固めると失敗が少ないです。

10. 状態管理(Zustand / Reduxなど)のテスト

状態管理は「UIより先に壊れる」ことが多いので、ここを単体テストできると強いです。 Zustandなら store を直接叩けます。

import { useStore } from "@/store"

test("increments count", () => {
  const before = useStore.getState().count
  useStore.getState().increment()
  expect(useStore.getState().count).toBe(before + 1)
})

Reduxでも同様に reducer / selector / thunk を単体で検証する発想が基本です。

11. カバレッジ(Coverage)を出して“見える化”する

テストが増えると「どこが未検証か」が分からなくなります。 そこでカバレッジをCIで出すと、チームが改善しやすくなります。

// vitest.config.ts(testの中)
coverage: {
  reporter: ["text", "html"],
}
npx vitest run --coverage
カバレッジは「高ければ偉い」ではなく、「穴を見つける地図」です。
重要箇所(決済、認証、料金計算など)に優先的にテストを当てる運用が現実的です。

12. CI(GitHub Actions)で自動化する

ローカルでしか動かないテストは、長期的に壊れます。 CIで毎回実行される状態が「品質の最低ライン」です。

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npx vitest run --coverage

※ Nodeのバージョンはプロジェクトに合わせて揃えてください(Next.jsの推奨に合わせるのが無難)。

13. E2E(Playwright)との棲み分け:初心者が迷わない結論

「UIテストもE2Eで全部やればよいのでは?」と思いがちですが、E2Eは重く、壊れやすいです。 おすすめの棲み分けは次の通りです。

E2Eは「保険」ではなく「最後の動作確認」。
大半の品質は、Vitestで安く・速く作るのが実務的です。

14. AI × Vitest:初心者チームほど効果が出やすい

最近はCopilotやChatGPTでテストを生成できますが、 生成物は「動かしてフィードバックする」ことで品質が上がります。 Vitestは速いので、AI生成→実行→修正のループが現実的に回ります。

AIを使うコツは「一発で正解を出させる」ではなく、
小さく生成→すぐ実行→失敗から修正を回すことです。 Vitestはこのサイクルを支えます。

15. Server Actions / RSC をどう設計してテストするか(fetch・cache・revalidate・cookies/headers)

App Router時代の最大の変化は「Server側で動く処理が増えた」ことです。 Server Actions、RSC内のfetch、revalidate、cookies/headers などは、 正しく設計しないとテスト不能なブラックボックスになります。

結論:
フレームワーク依存コードと、純粋ロジックを分離する。
テストするのは“振る舞い”であり、Next.js内部実装ではありません。

15-1. Server Actionの設計分離

よくある例:

// app/actions/createUser.ts
"use server"

import { db } from "@/lib/db"

export async function createUser(data: { name: string }) {
  if (!data.name) {
    throw new Error("Name required")
  }
  return db.user.create({ data })
}

このままではDB依存が強く、テストが難しいです。

改善例:

// lib/userService.ts
export function validateUserInput(name: string) {
  if (!name) throw new Error("Name required")
  return name.trim()
}
// app/actions/createUser.ts
"use server"

import { db } from "@/lib/db"
import { validateUserInput } from "@/lib/userService"

export async function createUser(data: { name: string }) {
  const name = validateUserInput(data.name)
  return db.user.create({ data: { name } })
}

これで、ビジネスロジックは単体テスト可能になります。

// lib/userService.test.ts
import { describe, it, expect } from "vitest"
import { validateUserInput } from "./userService"

describe("validateUserInput", () => {
  it("throws when empty", () => {
    expect(() => validateUserInput("")).toThrow()
  })

  it("trims whitespace", () => {
    expect(validateUserInput(" Alice ")).toBe("Alice")
  })
})

15-2. RSC内のfetchとキャッシュ戦略

RSCではfetchにキャッシュ戦略を指定できます。

await fetch("https://api.example.com/data", {
  cache: "no-store",
})

ここも同様に、直接fetchを呼ばずラップします。

// lib/apiClient.ts
export async function fetchData(fetchImpl = fetch) {
  const res = await fetchImpl("https://api.example.com/data", {
    cache: "no-store",
  })
  return res.json()
}
// lib/apiClient.test.ts
import { describe, it, expect, vi } from "vitest"
import { fetchData } from "./apiClient"

describe("fetchData", () => {
  it("calls fetch and returns json", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ ok: true }),
    })

    const result = await fetchData(mockFetch)
    expect(result.ok).toBe(true)
  })
})
fetchを依存注入すると、RSCでも純粋関数としてテストできます。

15-3. revalidatePath / revalidateTag の扱い

revalidate系はNext.jsのランタイム依存が強いため、 直接テスト対象にするのではなく、呼び出し箇所を抽象化します。

// lib/cache.ts
export function triggerRevalidate(fn: Function) {
  fn()
}

本番ではrevalidatePathを渡し、テストではmock関数を渡します。

15-4. cookies / headers のテスト戦略

RSC内で cookies() や headers() を使う場合、 それ自体を直接テストするのではなく、値を引数に渡す設計にします。

// lib/auth.ts
export function isAuthenticated(cookieValue: string | undefined) {
  return Boolean(cookieValue)
}
// lib/auth.test.ts
import { describe, it, expect } from "vitest"
import { isAuthenticated } from "./auth"

describe("isAuthenticated", () => {
  it("returns true when cookie exists", () => {
    expect(isAuthenticated("token")).toBe(true)
  })
})
Next.js依存APIは“境界”に置く。
テストするのは、境界の内側のロジック。

15-5. 実務での設計原則まとめ

これを守るだけで、App Router環境でも テスト不能なコードはほぼ消えます。

最終まとめ:Next.js時代におけるテスト設計の完成形

ここまで、Next.js(App Router)環境でのVitest活用を、 基礎から実務レベル、さらにServer Actions / RSC設計まで整理してきました。 最後に全体を体系化します。

■ 設計原則

■ 実務導入ステップ

  1. Vitest最小構成を導入
  2. 純粋ロジックからテストを追加
  3. UIコンポーネントへ拡張
  4. Route Handlerを単体検証
  5. MSWで通信を含むUI検証
  6. CI統合+Coverage可視化

■ App Router時代の考え方

Server ComponentsやServer Actionsは「直接テストする」のではなく、 内部ロジックを切り出して検証することが鍵です。

■ AIとの組み合わせ

Vitestは単なる高速テストツールではありません。
Next.js時代の「分離設計」と組み合わさって初めて真価を発揮します。

テストが自然に回る開発体制は、設計の健全さの証明でもあります。 Next.js × Vitest は、そのための合理的な基盤です。


← ブログTOPへ戻る