テストガイドライン
このプロジェクトでは、コード品質を保証するために包括的なテスト戦略を採用しています。
テスト戦略
テストの種類
-
ユニットテスト (Vitest)
- 個別の関数、コンポーネント、ユーティリティのテスト
- カバレッジ目標: 95%以上
-
E2Eテスト (Playwright)
- ユーザーシナリオに基づく統合テスト
- ページオブジェクトモデル(POM)パターンを採用
-
アクセシビリティテスト (Playwright + axe-core)
- WCAG準拠の検証
- Storybook上でのコンポーネントテスト
-
ビジュアルリグレッションテスト (Playwright)
- Storybook上でのスクリーンショット比較
-
インタラクションテスト (Playwright)
- Storybook上でのコンポーネントインタラクション検証
ユニットテスト (Vitest)
テストファイルの配置
テストファイルは、テスト対象のファイルと同じディレクトリに配置します。
app/├── shared/│ └── lib/│ ├── formatDate.ts│ └── formatDate.test.ts└── features/ └── blog-preview/ ├── ui/ │ ├── BlogPreview.tsx │ └── BlogPreview.test.tsx └── model/ ├── types.ts └── types.test.tsテストの書き方
関数のテスト
import { describe, expect, it } from "vitest";import { formatDate } from "./formatDate";
describe("formatDate", () => { it("should format date correctly", () => { const date = new Date("2024-01-01"); const result = formatDate(date); expect(result).toBe("2024-01-01"); });
it("should handle invalid date", () => { const date = new Date("invalid"); expect(() => formatDate(date)).toThrow(); });});Reactコンポーネントのテスト
import { render, screen } from "@testing-library/react";import { describe, expect, it } from "vitest";import { BlogPreview } from "./BlogPreview";
describe("BlogPreview", () => { it("should render blog preview", () => { const props = { title: "Test Post", slug: "test-post", date: "2024-01-01", };
render(<BlogPreview {...props} />);
expect(screen.getByText("Test Post")).toBeInTheDocument(); });
it("should render link with correct href", () => { const props = { title: "Test Post", slug: "test-post", date: "2024-01-01", };
render(<BlogPreview {...props} />);
const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "/blog/test-post"); });});Remixローダーのテスト
// app/routes/blog.$slug.test.tsimport { describe, expect, it, vi } from "vitest";import { loader } from "./blog.$slug";
describe("loader", () => { it("should return blog post data", async () => { const request = new Request("http://localhost/blog/test-post"); const params = { slug: "test-post" };
const response = await loader({ request, params, context: {} }); const data = await response.json();
expect(data.post).toBeDefined(); expect(data.post.slug).toBe("test-post"); });
it("should return 404 for non-existent post", async () => { const request = new Request("http://localhost/blog/non-existent"); const params = { slug: "non-existent" };
await expect( loader({ request, params, context: {} }) ).rejects.toThrow(); });});テストユーティリティ
共通のテストユーティリティは testing/vitest/ に配置されています。
// testing/vitest/setup.ts で設定される共通のセットアップimport "@testing-library/jest-dom";import { cleanup } from "@testing-library/react";import { afterEach } from "vitest";
afterEach(() => { cleanup();});カバレッジ設定
カバレッジレポートは次の設定で生成されます:
- レポート形式: HTML, LCOV
- 閾値: 80%(lines, functions, branches, statements)
- 除外:
.cache/,node_modules/,**/*.test.{ts,tsx},**/*.config.{ts,js}
# カバレッジレポートを生成bun run coverage
# 特定のパッケージのカバレッジturbo run coverage --filter=@portfolio/webE2Eテスト (Playwright)
ページオブジェクトモデル(POM)
E2Eテストでは、ページオブジェクトモデルパターンを採用しています。
ページオブジェクトの構造
import { Page } from "@playwright/test";import { BasePage } from "./base.page";
export class HomePage extends BasePage { constructor(page: Page) { super(page); }
async gotoHome(): Promise<void> { await this.page.goto("/"); }
async expectHeroVisible(): Promise<void> { await expect(this.page.locator("[data-testid='hero']")).toBeVisible(); }
async clickBlogLink(): Promise<void> { await this.page.getByRole("link", { name: "Blog" }).click(); }}テストの書き方
import { expect, test } from "@playwright/test";import { HomePage } from "./pages/home.page";
test("should display home page", async ({ page }) => { const homePage = new HomePage(page); await homePage.gotoHome(); await homePage.expectHeroVisible();});
test("should navigate to blog page", async ({ page }) => { const homePage = new HomePage(page); await homePage.gotoHome(); await homePage.clickBlogLink();
await expect(page).toHaveURL(/\/blog/);});APIテスト
APIエンドポイントのレスポンスを検証するテストも含まれています。
import { expect, test } from "@playwright/test";
test("should return blog posts", async ({ request }) => { const response = await request.get("/api/blog"); expect(response.ok()).toBeTruthy();
const data = await response.json(); expect(Array.isArray(data)).toBeTruthy();});
test("should return 404 for non-existent post", async ({ request }) => { const response = await request.get("/api/blog/non-existent"); expect(response.status()).toBe(404);});アクセシビリティテスト
Storybook上でコンポーネントのアクセシビリティを検証します。
import { expect, test } from "@playwright/test";
test("BlogPreview should be accessible", async ({ page }) => { await page.goto("/iframe.html?id=features-blogpreview--default");
const violations = await page.evaluate(() => { // axe-coreによるアクセシビリティチェック return window.axe?.run(); });
expect(violations).toBeUndefined();});ビジュアルリグレッションテスト
Storybook上でスクリーンショット比較を行います。
import { expect, test } from "@playwright/test";
test("BlogPreview visual regression", async ({ page }) => { await page.goto("/iframe.html?id=features-blogpreview--default");
await expect(page).toHaveScreenshot("blog-preview.png");});テスト実行
ローカル環境
# すべてのユニットテストを実行bun run test
# 特定のパッケージのテストturbo run test --filter=@portfolio/web
# ウォッチモードbun vitest
# カバレッジレポート生成bun run coverage
# E2Eテスト実行bun run e2e
# アクセシビリティテストbun run accessibility
# ビジュアルリグレッションテストbun run visual
# インタラクションテストbun run interactionsCI環境
CI環境では、Dockerコンテナ内でテストが実行されます。
# CI環境でのE2Eテスト実行例docker run --rm -e CI=true \ -v $(pwd):/work -w /work \ -v $(pwd)/node_modules:/work/node_modules \ e2e bunx playwright testテストのベストプラクティス
1. テストの独立性
各テストは独立して実行可能であるべきです。
// ✅ Good: 各テストが独立しているdescribe("formatDate", () => { it("should format date correctly", () => { // テスト1 });
it("should handle invalid date", () => { // テスト2(テスト1に依存しない) });});2. 明確なテスト名
テスト名は、何をテストしているかが明確であるべきです。
// ✅ Good: 明確なテスト名it("should return 404 when blog post does not exist", () => { // ...});
// ❌ Bad: 曖昧なテスト名it("should work", () => { // ...});3. AAAパターン
テストは Arrange(準備)、Act(実行)、Assert(検証)の3つのセクションに分けます。
it("should format date correctly", () => { // Arrange: テストデータの準備 const date = new Date("2024-01-01");
// Act: テスト対象の実行 const result = formatDate(date);
// Assert: 結果の検証 expect(result).toBe("2024-01-01");});4. モックの適切な使用
外部依存関係は適切にモックします。
import { vi } from "vitest";
it("should fetch data from API", async () => { // API呼び出しをモック const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: "test" }), });
global.fetch = mockFetch;
const result = await fetchData(); expect(result.data).toBe("test"); expect(mockFetch).toHaveBeenCalledWith("/api/data");});5. エッジケースのテスト
正常系だけでなく、エッジケースもテストします。
describe("formatDate", () => { it("should handle normal date", () => { // 正常系 });
it("should handle invalid date", () => { // エッジケース1 });
it("should handle null", () => { // エッジケース2 });
it("should handle empty string", () => { // エッジケース3 });});テストカバレッジ
カバレッジ目標
- 全体: 80%以上
- 新規コード: 90%以上
カバレッジレポートの確認
# HTMLレポートを開くopen docs/vitest/coverage/index.html
# LCOVレポート(CI用)cat docs/vitest/coverage/lcov.info