Advanced (更新: 2026/6/2)

Claude Codeでテスト戦略を作る実践ガイド: Vitest、Testing Library、Playwright、CIまで

Claude Codeで単体・結合・E2E・CIを設計し、壊れにくいテストを増やす実践手順。

Claude Codeでテスト戦略を作る実践ガイド: Vitest、Testing Library、Playwright、CIまで

テスト戦略は「テストをたくさん書くこと」ではありません。何を単体テストで守り、どこを結合テストで確認し、どのユーザー導線だけをE2Eで通すかを決めることです。

Claude Codeに「テストを追加して」とだけ頼むと、見た目は増えても価値の薄いテストになりがちです。たとえば実装の内部関数だけを固定する、CSSセレクタに依存する、DBの初期化を忘れる、CIでは再現しない前提で書く、といった失敗が起きます。

この記事では、初心者でもそのまま試せる形で、Vitest、Testing Library、Playwright、GitHub Actionsを使ったテスト戦略を整理します。公式情報はClaude CodeのCommon workflowsVitestのCoverageガイドTesting LibraryのQueriesPlaywrightのLocatorsCIガイドを確認しています。

最初に決めるテストピラミッド

テストピラミッドは、下に速い単体テスト、中央に結合テスト、上に少数のE2Eテストを置く考え方です。ピラミッドという名前ですが、絶対比率のルールではなく、変更頻度と失敗時の原因特定しやすさを分けるための地図です。

flowchart TB
  E2E["E2E: 購入、登録、ログインなどの主要導線"]
  INT["結合: API、DB、フォーム、外部境界"]
  UNIT["単体: 価格計算、権限判定、入力検証"]
  E2E --> INT --> UNIT
レイヤー目安守るもの主なツール
単体テスト60-70%計算、分岐、バリデーション、権限判定Vitest
結合テスト20-30%Reactコンポーネント、API、DB境界Vitest + Testing Library
E2Eテスト5-10%収益・登録・購入などの主要導線Playwright
CIの品質ゲート全PRlint、型、単体、E2E、レポート保存GitHub Actions

Claude Codeに依頼する前に、対象機能をこの表へ落とします。たとえば価格計算は単体、在庫切れボタンは結合、購入完了までの流れはE2Eです。全部をE2Eで守ると遅く、壊れた時に原因が見えません。逆に単体テストだけだと、画面から実際に購入できるかは分かりません。

最小構成を先に固定する

まずテストの実行コマンドを固定します。Claude Codeは既存のpackage.jsonやテストファイルを読んで寄せてくれますが、最初の足場が曖昧だとフレームワークが混ざります。

npm i -D vitest @vitest/coverage-v8 jsdom \
  @testing-library/react @testing-library/jest-dom @testing-library/user-event \
  @playwright/test
npx playwright install --with-deps
{
  "scripts": {
    "test": "vitest --run",
    "test:watch": "vitest",
    "test:coverage": "vitest --run --coverage",
    "test:e2e": "playwright test"
  }
}

Vitestのcoverage providerは、公式ドキュメントでv8istanbulが案内されています。2026年時点では、NodeやChromiumなどV8環境ならv8が標準的な選択です。

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: ["./test/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
      exclude: ["dist/**", "coverage/**", "playwright-report/**"],
    },
  },
});
// test/setup.ts
import "@testing-library/jest-dom/vitest";

実例1: 価格計算を単体テストで守る

単体テストは、外部I/Oを持たない純粋なロジックに向いています。ここでは、記事販売やSaaS課金でよくある税込・割引計算を扱います。

// src/lib/pricing.ts
export type PriceInput = {
  unitPrice: number;
  quantity: number;
  discountRate?: number;
  taxRate?: number;
};

export function calculateTotal({
  unitPrice,
  quantity,
  discountRate = 0,
  taxRate = 0.1,
}: PriceInput): number {
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new Error("quantity must be a non-negative integer");
  }
  if (unitPrice < 0) {
    throw new Error("unitPrice must be non-negative");
  }
  if (discountRate < 0 || discountRate > 1) {
    throw new Error("discountRate must be between 0 and 1");
  }
  if (taxRate < 0 || taxRate > 1) {
    throw new Error("taxRate must be between 0 and 1");
  }

  const subtotal = unitPrice * quantity;
  const discounted = subtotal * (1 - discountRate);
  return Math.round(discounted * (1 + taxRate));
}
// src/lib/pricing.test.ts
import { describe, expect, it } from "vitest";
import { calculateTotal } from "./pricing";

describe("calculateTotal", () => {
  it("calculates tax-included total", () => {
    expect(calculateTotal({ unitPrice: 1000, quantity: 2 })).toBe(2200);
  });

  it("applies discount before tax", () => {
    expect(
      calculateTotal({ unitPrice: 1000, quantity: 2, discountRate: 0.2 })
    ).toBe(1760);
  });

  it("allows zero quantity", () => {
    expect(calculateTotal({ unitPrice: 1000, quantity: 0 })).toBe(0);
  });

  it("rejects invalid quantity and rates", () => {
    expect(() => calculateTotal({ unitPrice: 1000, quantity: -1 })).toThrow(
      "quantity must be a non-negative integer"
    );
    expect(() =>
      calculateTotal({ unitPrice: 1000, quantity: 1, discountRate: 1.2 })
    ).toThrow("discountRate must be between 0 and 1");
  });
});

落とし穴は、カバレッジ80%を目的にして正常系だけを増やすことです。価格計算で本当に怖いのは、数量0、100%割引、不正な割引率、丸め処理です。Claude Codeには「正常系、境界値、異常系を分けて」と明示します。

実例2: CTAボタンをTesting Libraryで確認する

Testing Libraryでは、ユーザーが見つける方法に近いクエリを優先します。公式ドキュメントでもgetByRolegetByLabelTextのようなアクセシブルなクエリが優先されています。これはアクセシビリティだけでなく、テストの壊れにくさにも効きます。

// src/components/CheckoutButton.tsx
import { useState } from "react";

type CheckoutButtonProps = {
  stock: number;
  onCheckout: () => Promise<void>;
};

export function CheckoutButton({ stock, onCheckout }: CheckoutButtonProps) {
  const [submitting, setSubmitting] = useState(false);
  const soldOut = stock <= 0;

  async function handleClick() {
    setSubmitting(true);
    try {
      await onCheckout();
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <button
      type="button"
      disabled={soldOut || submitting}
      aria-busy={submitting}
      onClick={handleClick}
    >
      {soldOut ? "Sold out" : submitting ? "Processing..." : "Buy now"}
    </button>
  );
}
// src/components/CheckoutButton.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { CheckoutButton } from "./CheckoutButton";

describe("CheckoutButton", () => {
  it("calls checkout when the product is in stock", async () => {
    const user = userEvent.setup();
    const onCheckout = vi.fn().mockResolvedValue(undefined);

    render(<CheckoutButton stock={3} onCheckout={onCheckout} />);

    await user.click(screen.getByRole("button", { name: "Buy now" }));

    expect(onCheckout).toHaveBeenCalledTimes(1);
  });

  it("disables checkout when sold out", async () => {
    const user = userEvent.setup();
    const onCheckout = vi.fn().mockResolvedValue(undefined);

    render(<CheckoutButton stock={0} onCheckout={onCheckout} />);

    const button = screen.getByRole("button", { name: "Sold out" });
    expect(button).toBeDisabled();
    await user.click(button);
    expect(onCheckout).not.toHaveBeenCalled();
  });
});

ここでcontainer.querySelector(".primary-button")を使うと、デザイン変更だけでテストが壊れます。CTAは収益導線なので、ボタンの役割と表示名で守るほうが実務向きです。アクセシビリティ観点はClaude Codeアクセシビリティ改善も参考になります。

実例3: Playwrightで購入導線だけをE2Eにする

E2Eは便利ですが、増やしすぎるとCIを遅くします。ClaudeCodeLabのようなブログや教材サイトなら、全ページの細かい表示ではなく、無料PDF、商品ページ、相談CTA、チェックアウトのような収益に近い導線を優先します。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

const baseURL =
  process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://127.0.0.1:5173";

export default defineConfig({
  testDir: "./e2e",
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL,
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  webServer: process.env.PLAYWRIGHT_TEST_BASE_URL
    ? undefined
    : {
        command: "npm run dev -- --host 127.0.0.1",
        url: baseURL,
        reuseExistingServer: !process.env.CI,
      },
});
// e2e/monetization-flow.spec.ts
import { expect, test } from "@playwright/test";

test("reader can move from article CTA to products", async ({ page }) => {
  await page.goto("/blog/claude-code-testing-strategies");

  const cta = page.getByRole("link", {
    name: /Claude Code実践テンプレート|products/i,
  });
  await expect(cta).toBeVisible();

  await cta.click();
  await expect(page).toHaveURL(/\/products\/?$/);
  await expect(
    page.getByRole("heading", { name: /products|プロダクト/i })
  ).toBeVisible();
});

Playwrightのexpect(locator).toBeVisible()のようなアサーションは自動リトライされます。waitForTimeout(3000)で待つより、ユーザーが見る状態を待つほうが安定します。ロケータもgetByRolegetByLabel、必要なら安定したdata-testidに寄せ、.nth(2)や深いCSSパスを避けます。

実例4: CIでテストを品質ゲートにする

ローカルだけでテストが通っても、公開前の品質は守れません。PRごとにlint、型チェック、単体テスト、E2Eを実行し、失敗時のレポートを保存します。

# .github/workflows/test.yml
name: Test

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm run test:coverage

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npm run test:e2e
        env:
          CI: "true"

      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Claude Code GitHub Actionsを使う場合は、公式ドキュメントでGA版のanthropics/claude-code-action@v1が案内されています。古い@beta前提の記事を真似しないようにします。CI/CD全体の組み方はClaude CodeでCI/CDパイプラインを構築する方法も合わせて確認してください。

Claude Codeへの依頼テンプレート

次のテンプレートは、そのままClaude Codeへ貼り付けて使えます。ポイントは、対象ファイル、テストレイヤー、実行コマンド、触ってよい範囲を明示することです。

依頼1: 単体テストの追加
src/lib/pricing.ts を読み、Vitestで単体テストを追加してください。
正常系、境界値、異常系を分けてください。
実装は変更せず、必要ならバグ候補を先に箇条書きしてください。
最後に npm run test -- pricing を実行し、結果を報告してください。
依頼2: Reactコンポーネントの結合テスト
src/components/CheckoutButton.tsx に対して Testing Library のテストを追加してください。
getByRole と userEvent.setup() を使い、CSSクラスや snapshot に依存しないでください。
在庫あり、在庫なし、送信中の3ケースを確認してください。
依頼3: PlaywrightのE2Eを最小追加
収益導線だけを対象に Playwright テストを1本追加してください。
対象は記事ページから /products/ へのCTA遷移です。
waitForTimeout、深いCSSセレクタ、nth() を避けてください。
失敗時に追えるよう trace: on-first-retry 前提で書いてください。
依頼4: CI失敗の原因調査
直近のCIログを読み、失敗を lint、typecheck、unit、e2e、環境差分に分類してください。
修正は最小差分にしてください。
テストを弱くする、skipする、タイムアウトだけ伸ばす修正は提案しないでください。

具体的な落とし穴

一つ目は、モックしすぎることです。決済APIやメール送信はモックでよいですが、価格計算、DB保存、権限判定まで全部モックすると、成功してほしいものだけを確認するテストになります。

二つ目は、E2Eで画面の見た目を細かく固定することです。hero imgの順番やカードの個数を固定すると、記事改善やCTA改善のたびに壊れます。E2Eでは「読者が次の行動へ進めるか」を守ります。

三つ目は、カバレッジの数字だけを見ることです。80%は入り口であって、品質の証明ではありません。未テストの分岐が支払い、認証、削除、公開処理に近いなら、数字より優先して追加します。

四つ目は、Claude Codeに失敗例を渡さないことです。「良いテストを書いて」では曖昧です。「CSSクラスに依存しない」「snapshotだけで済ませない」「skipしない」「CIで再現できるようにする」と禁止事項も渡すと出力が安定します。

五つ目は、公式ドキュメントを見ずに古いサンプルをコピーすることです。PlaywrightのCI例、Vitestのcoverage provider、Claude Code GitHub Actionsの入力名は変わる可能性があります。記事更新時は、必ず公式ページを見直します。

もう一つよくあるのは、テストを書く順番を「目立つ画面」から決めることです。トップページの見た目をE2Eで守っても、価格計算、権限判定、フォーム送信、メール配信の失敗には気づけません。Claude Codeに依頼するときは、まず「壊れると売上・問い合わせ・信用に近い順」を並べ、単体テストで守れるもの、結合テストが必要なもの、E2Eまで必要なものを分けさせると、少ないテストでも効果が出ます。

収益導線CTA

テスト戦略は、品質だけでなく収益にも効きます。無料PDFの登録、Gumroadの商品ページ、研修相談フォームが壊れていると、どれだけ記事を増やしても成果につながりません。まずは無料チートシートでClaude Codeの基本コマンドを固定し、繰り返し使うレビュー観点はClaude Code実践テンプレートにまとめ、チームで導入する場合はClaude Code研修・導入相談でCIとレビュー基準まで整えるのが現実的です。

テストを増やす順番は、単体、結合、E2E、CIです。いきなり大きな自動化を作るより、壊れると売上や信頼に近い場所から守ります。TDDで進めたい場合はClaude CodeとTDDの実践、不具合調査にテストを使う場合はClaude Codeデバッグテクニックも役に立ちます。

この記事で紹介した内容を実際に試した結果

MasaがClaudeCodeLabの記事CTAと商品ページ導線で試したとき、一番効果があったのは「E2Eを増やす」ことではなく、単体テストで価格計算を固定し、Testing LibraryでCTAの役割と文言を守り、Playwrightは記事から商品ページへ進める1本に絞ることでした。Claude Codeには最初に失敗例を列挙し、最後にnpm run test:coveragenpm run test:e2eの結果を要約させると、skipや脆いCSSセレクタが減りました。残った課題は、外部決済や広告タグのようにローカルで完全再現しにくい部分です。ここはCIのレポートと手動確認を組み合わせる必要があります。

#Claude Code #testing #test strategy #Vitest #Playwright
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。