mcpField
T
testing-strategy

testing-strategy

github.com/mgd34msuテスト・品質657 回インストール10 週間前に更新
リポジトリ

testing-strategy について

アプリケーションのテストを追加・改善する開発者向けのスキルです。ユニット、コンポーネント、統合、E2Eテストを扱い、主要フレームワークの例とカバレッジ、CI、モック、フレーキーテストの対策を示します。

主な機能

  1. 01ユニット・コンポーネントテストのパターン
  2. 02統合・E2Eテストの案内
  3. 03Vitest、Jest、Playwrightの例
  4. 04MSWによるAPIモック
  5. 05カバレッジとCIの実践
  6. 06フレーキーテストの調査

活用シーン

  1. 01テスト基盤のセットアップ
  2. 02Reactコンポーネントやフックのテスト追加
  3. 03APIルートとデータベース操作のテスト
  4. 04カバレッジ不足や不安定なテストの発見

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill testing-strategy

インストール後、Claude Code で/testing-strategyを入力して読み込まれたか確認します。

同梱ファイルreferences/testing-patterns.mdscripts/validate-tests.sh

詳細

リソース

scripts/
  validate-tests.sh
references/
  testing-patterns.md

テスト戦略

このスキルでは、最新のテストフレームワークと GoodVibes の高精度ツールを使用して、包括的なテスト戦略を実装する方法を説明します。既存のコードにテストを追加する場合、テスト基盤を設定する場合、またはカバレッジ目標を達成する場合に、このワークフローを使用してください。

このスキルを使用する場面

  • テスト基盤の設定(Vitest、Jest、Playwright)
  • 関数、フック、ユーティリティの単体テストの作成
  • React Testing Library を使ったコンポーネントテストの作成
  • Playwright を使った E2E テストの実装
  • MSW を使った API モックの追加
  • テストカバレッジの向上
  • CI テストパイプラインの設定
  • 不安定なテストのデバッグ
  • テストフィクスチャとファクトリーの作成

テストの構成

ファイルの命名規則

一貫した命名パターンに従います。

// Co-located pattern (recommended)
src/
  components/
    Button.tsx
    Button.test.tsx          // Component tests
  utils/
    formatDate.ts
    formatDate.test.ts       // Unit tests
  api/
    users.ts
    users.integration.test.ts // Integration tests

// Centralized pattern (alternative)
__tests__/
  components/
    Button.test.tsx
  utils/
    formatDate.test.ts
テストスイートの構成

明確な describe ブロックを使ってテストを整理します。

import { describe, it, expect, beforeEach } from 'vitest';
import { formatDate } from './formatDate';

describe('formatDate', () => {
  describe('with valid dates', () => {
    it('formats ISO dates to MM/DD/YYYY', () => {
      expect(formatDate('2024-01-15')).toBe('01/15/2024');
    });

    it('handles Date objects', () => {
      const date = new Date('2024-01-15');
      expect(formatDate(date)).toBe('01/15/2024');
    });
  });

  describe('with invalid dates', () => {
    it('throws for invalid strings', () => {
      expect(() => formatDate('invalid')).toThrow('Invalid date');
    });

    it('throws for null', () => {
      expect(() => formatDate(null)).toThrow('Invalid date');
    });
  });
});
検出:テストファイルの検索

高精度ツールを使って、既存のテストパターンを検出します。

# Find all test files and analyze patterns
discover:
  queries:
    - id: test-files
      type: glob
      patterns:
        - "**/*.test.{ts,tsx,js,jsx}"
        - "**/*.spec.{ts,tsx,js,jsx}"
        - "e2e/**/*.spec.ts"
      exclude:
        - "**/node_modules/**"
        - "**/dist/**"
    - id: test-config
      type: glob
      patterns:
        - "vitest.config.ts"
        - "jest.config.js"
        - "playwright.config.ts"
    - id: skipped-tests
      type: grep
      pattern: "\\.skip|it\\.only|describe\\.only"
      glob: "**/*.test.{ts,tsx}"
  output_mode: files_only

単体テスト

Vitest のセットアップ(推奨)
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/dist/**',
      ],
      all: true,
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
純粋関数のテスト
import { describe, it, expect } from 'vitest';
import { calculateTotal, discountPrice } from './pricing';

describe('pricing utilities', () => {
  describe('calculateTotal', () => {
    it('sums item prices', () => {
      const items = [
        { price: 10.00, quantity: 2 },
        { price: 5.50, quantity: 1 },
      ];
      expect(calculateTotal(items)).toBe(25.50);
    });

    it('returns 0 for empty array', () => {
      expect(calculateTotal([])).toBe(0);
    });

    it('handles quantity multipliers', () => {
      const items = [{ price: 10, quantity: 3 }];
      expect(calculateTotal(items)).toBe(30);
    });
  });

  describe('discountPrice', () => {
    it('applies percentage discount', () => {
      expect(discountPrice(100, 0.2)).toBe(80);
    });

    it('rounds to 2 decimal places', () => {
      expect(discountPrice(10.99, 0.15)).toBe(9.34);
    });

    it('throws for invalid discounts', () => {
      expect(() => discountPrice(100, -0.1)).toThrow('Invalid discount');
      expect(() => discountPrice(100, 1.5)).toThrow('Invalid discount');
    });
  });
});
React フックのテスト
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useAuth } from './useAuth';

// Mock the auth context
vi.mock('@/contexts/AuthContext', () => ({
  useAuthContext: vi.fn(),
}));

describe('useAuth', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns user when authenticated', () => {
    vi.mocked(useAuthContext).mockReturnValue({
      user: { id: '1', email: '[email protected]' },
      isLoading: false,
    });

    const { result } = renderHook(() => useAuth());
    
    expect(result.current.user).toEqual({
      id: '1',
      email: '[email protected]',
    });
    expect(result.current.isAuthenticated).toBe(true);
  });

  it('handles loading state', () => {
    vi.mocked(useAuthContext).mockReturnValue({
      user: null,
      isLoading: true,
    });

    const { result } = renderHook(() => useAuth());
    
    expect(result.current.isLoading).toBe(true);
    expect(result.current.isAuthenticated).toBe(false);
  });

  it('refetches user on login', async () => {
    const refetch = vi.fn().mockResolvedValue({ id: '1' });
    vi.mocked(useAuthContext).mockReturnValue({
      user: null,
      refetch,
    });

    const { result } = renderHook(() => useAuth());
    
    await result.current.login('[email protected]', 'password');
    
    expect(refetch).toHaveBeenCalledOnce();
  });
});

コンポーネントテスト

React Testing Library のパターン
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
import { SearchInput } from './SearchInput';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();
    
    render(<Button onClick={handleClick}>Click</Button>);
    await user.click(screen.getByRole('button'));
    
    expect(handleClick).toHaveBeenCalledOnce();
  });

  it('disables button when loading', () => {
    render(<Button isLoading>Submit</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });

  it('shows spinner when loading', () => {
    render(<Button isLoading>Submit</Button>);
    expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
  });

  it('applies variant styles', () => {
    const { container } = render(<Button variant="primary">Primary</Button>);
    expect(container.firstChild).toHaveClass('bg-blue-600');
  });
});

describe('SearchInput', () => {
  it('debounces search input', async () => {
    const onSearch = vi.fn();
    const user = userEvent.setup();
    
    render(<SearchInput onSearch={onSearch} debounce={300} />);
    const input = screen.getByRole('searchbox');
    
    await user.type(input, 'test query');
    
    // Should not call immediately
    expect(onSearch).not.toHaveBeenCalled();
    
    // Should call after debounce
    await waitFor(
      () => expect(onSearch).toHaveBeenCalledWith('test query'),
      { timeout: 500 }
    );
  });

  it('clears input on clear button click', async () => {
    const user = userEvent.setup();
    
    render(<SearchInput />);
    const input = screen.getByRole('searchbox') as HTMLInputElement;
    
    await user.type(input, 'test');
    expect(input.value).toBe('test');
    
    await user.click(screen.getByRole('button', { name: /clear/i }));
    expect(input.value).toBe('');
  });
});
非同期コンポーネントのテスト
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserProfile } from './UserProfile';
import * as api from '@/api/users';

vi.mock('@/api/users');

describe('UserProfile', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('shows loading state initially', () => {
    vi.mocked(api.getUser).mockReturnValue(new Promise(() => {}));
    
    render(<UserProfile userId="1" />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('displays user data when loaded', async () => {
    vi.mocked(api.getUser).mockResolvedValue({
      id: '1',
      name: 'John Doe',
      email: '[email protected]',
    });
    
    render(<UserProfile userId="1" />);
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
    
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
  });

  it('shows error message on fetch failure', async () => {
    vi.mocked(api.getUser).mockRejectedValue(new Error('Failed to fetch'));
    
    render(<UserProfile userId="1" />);
    
    await waitFor(() => {
      expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
    });
  });
});

統合テスト

API ルートのテスト
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createMocks } from 'node-mocks-http';
import { POST } from '@/app/api/posts/route';
import { prisma } from '@/lib/prisma';
import { createTestUser, cleanupDatabase } from '@/test/helpers';

describe('POST /api/posts', () => {
  let testUser: { id: string; email: string };

  beforeAll(async () => {
    testUser = await createTestUser();
  });

  afterAll(async () => {
    await cleanupDatabase();
  });

  it('creates a new post', async () => {
    const { req } = createMocks({
      method: 'POST',
      body: {
        title: 'Test Post',
        content: 'Test content',
      },
      headers: {
        authorization: `Bearer ${testUser.token}`,
      },
    });

    const response = await POST(req);
    const data = await response.json();

    expect(response.status).toBe(201);
    expect(data).toMatchObject({
      title: 'Test Post',
      content: 'Test content',
      authorId: testUser.id,
    });

    // Verify in database
    const post = await prisma.post.findUnique({
      where: { id: data.id },
    });
    expect(post).toBeTruthy();
  });

  it('validates required fields', async () => {
    const { req } = createMocks({
      method: 'POST',
      body: { title: '' }, // Missing content
      headers: { authorization: `Bearer ${testUser.token}` },
    });

    const response = await POST(req);
    const data = await response.json();

    expect(response.status).toBe(400);
    expect(data.error).toMatchObject({
      fieldErrors: {
        title: expect.arrayContaining([expect.any(String)]),
        content: expect.arrayContaining([expect.any(String)]),
      },
    });
  });

  it('requires authentication', async () => {
    const { req } = createMocks({
      method: 'POST',
      body: { title: 'Test', content: 'Test' },
      // No authorization header
    });

    const response = await POST(req);
    expect(response.status).toBe(401);
  });
});
フィクスチャを使ったデータベーステスト
// test/helpers.ts
import { PrismaClient } from '@prisma/client';
import { hash } from 'bcrypt';

export const prisma = new PrismaClient();

export async function createTestUser(overrides = {}) {
  const hashedPassword = await hash('password123', 10);
  
  return prisma.user.create({
    data: {
      email: `test-${Date.now()}@example.com`,
      name: 'Test User',
      password: hashedPassword,
      ...overrides,
    },
  });
}

export async function createTestPost(userId: string, overrides = {}) {
  return prisma.post.create({
    data: {
      title: 'Test Post',
      content: 'Test content',
      authorId: userId,
      published: false,
      ...overrides,
    },
  });
}

export async function cleanupDatabase() {
  // Delete in correct order to respect foreign keys
  await prisma.comment.deleteMany();
  await prisma.post.deleteMany();
  await prisma.user.deleteMany();
}

E2E テスト

Playwright のセットアップ
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});
ページオブジェクトパターン
// e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign in' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectError(message: string) {
    // Note: expect is imported from @playwright/test
    await expect(this.errorMessage).toContainText(message);
  }
}

// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test.describe('Authentication', () => {
  test('successful login redirects to dashboard', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('[email protected]', 'password123');
    
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Welcome back')).toBeVisible();
  });

  test('invalid credentials show error', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('[email protected]', 'wrong');
    
    await loginPage.expectError('Invalid email or password');
  });

  test('preserves redirect after login', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page).toHaveURL(/\/login\?redirect=%2Fdashboard/);
    
    const loginPage = new LoginPage(page);
    await loginPage.login('[email protected]', 'password123');
    
    await expect(page).toHaveURL('/dashboard');
  });
});

モックのパターン

API モックに MSW を使用する
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/user', () => {
    return HttpResponse.json({
      id: '1',
      email: '[email protected]',
      name: 'Test User',
    });
  }),

  http.post('/api/posts', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      {
        id: '1',
        ...body,
        createdAt: new Date().toISOString(),
      },
      { status: 201 }
    );
  }),

  http.get('/api/posts/:id', ({ params }) => {
    const { id } = params;
    
    if (id === '404') {
      return new HttpResponse(null, { status: 404 });
    }
    
    return HttpResponse.json({
      id,
      title: 'Test Post',
      content: 'Test content',
    });
  }),
];

// src/test/setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
モジュールのモック
import { vi } from 'vitest';
import type * as PrismaModule from '@/lib/prisma';

// Mock entire module
vi.mock('@/lib/prisma', () => ({
  prisma: {
    user: {
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    post: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
  },
}));

// Mock with implementation
vi.mock('@/lib/auth', () => ({
  hashPassword: vi.fn((password: string) => `hashed_${password}`),
  verifyPassword: vi.fn((password: string, hash: string) => {
    return hash === `hashed_${password}`;
  }),
}));

// Partial mock
vi.mock('@/lib/email', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/lib/email')>();
  return {
    ...actual,
    sendEmail: vi.fn(), // Mock only sendEmail
  };
});

カバレッジ戦略

目標しきい値

新しいコードでは、高いカバレッジを目指します。

  • 新しいコードは 100%(CI で強制)
  • 既存のコードは 80% 以上(段階的に改善)
  • 重要なパスは 100%(認証、決済、データ整合性)
カバレッジの設定
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json-summary', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/dist/**',
        '**/*.stories.tsx', // Storybook
        '**/types/**',
      ],
      all: true,
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
      // Fail CI if coverage drops
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },
  },
});
カバレッジの不足箇所を特定する

高精度ツールを使って、テストされていないコードを見つけます。

# Find files missing tests
precision_exec:
  commands:
    - cmd: "npm run test:coverage -- --reporter=json-summary"
      expect:
        exit_code: 0

# Parse coverage report
precision_read:
  files:
    - path: "coverage/coverage-summary.json"
  extract: content
  verbosity: minimal

# Find source files without corresponding tests
discover:
  queries:
    - id: source-files
      type: glob
      patterns: ["src/**/*.{ts,tsx}"]
      exclude: ["**/*.test.*", "**/*.spec.*"]
    - id: test-files
      type: glob
      patterns: ["**/*.test.{ts,tsx}"]
  output_mode: files_only

CI との統合

GitHub Actions の例
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - run: npm ci
      
      - name: Run unit tests
        run: npm run test:coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json
      
      - name: Run E2E tests
        run: npx playwright test
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
テストの並列実行
// vitest.config.ts - parallel by default
export default defineConfig({
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false,
        maxThreads: 8,
      },
    },
  },
});

// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 1 : 4, // Limit parallelism in CI
  fullyParallel: true,
});

高精度ツールとの統合

期待値を指定してテストを実行する
# Run tests and validate output
precision_exec:
  commands:
    - cmd: "npm run test -- --run"
      expect:
        exit_code: 0
    
    - cmd: "npm run typecheck"
      expect:
        exit_code: 0
    
    - cmd: "npm run test:coverage -- --run"
      expect:
        exit_code: 0
        stdout_contains: "All files"
  
  verbosity: minimal
テストの一括検証
batch:
  id: validate-tests
  operations:
    query:
      - id: find-skipped
        type: grep
        pattern: "\\.skip|it\\.only|describe\\.only"
        glob: "**/*.test.ts"
        output:
          format: count_only
    
    exec:
      - id: run-tests
        type: command
        commands:
          - cmd: "npm run test -- --run"
            expect: { exit_code: 0 }
      
      - id: check-coverage
        type: command
        commands:
          - cmd: "npm run test:coverage -- --run"
            expect:
              exit_code: 0
              stdout_contains: "All files"
  
  config:
    execution:
      mode: sequential

不安定なテストのデバッグ

よくある原因
  1. 競合状態:非同期処理には waitFor を使用します
  2. 時間依存のテスト:vi.useFakeTimers() でタイマーをモックします
  3. テストの分離:テスト間で状態が共有されないようにします
  4. ネットワークリクエスト:実際の API に依存せず、MSW でモックします
  5. ランダムデータ:決定的なテストデータを使用するか、ランダム生成器にシード値を設定します
不安定なテストのパターン
import { vi, beforeEach, afterEach } from 'vitest';

// Mock timers for time-dependent code
beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.restoreAllMocks();
});

it('debounces function calls', async () => {
  const callback = vi.fn();
  const debounced = debounce(callback, 1000);
  
  debounced('test');
  expect(callback).not.toHaveBeenCalled();
  
  vi.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalledWith('test');
});

// Ensure test isolation
beforeEach(async () => {
  await cleanupDatabase();
  vi.clearAllMocks();
});

// Use deterministic data
it('sorts users by creation date', () => {
  const users = [
    { id: '1', createdAt: new Date('2024-01-01') },
    { id: '2', createdAt: new Date('2024-01-02') },
  ];
  
  const sorted = sortByDate(users);
  expect(sorted[0].id).toBe('2');
});

ベストプラクティス

  1. 新機能では、まずテストを書く(TDD)
  2. 実装ではなく振る舞いをテストする — 方法ではなく、何をするかに焦点を当てます
  3. 明確さを保つため、可能であれば テストごとにアサーションを 1 つにする
  4. わかりやすいテスト名を付ける — 「Y のときに X する」
  5. あらゆるものをモックしない — 統合テストでは実際の連携先が必要です
  6. テストを高速に保つ — 単体テストは 100ms 未満、統合テストは 1 秒未満
  7. エッジケースをテストする — null、undefined、空の配列、境界値
  8. フレームワークのコードをテストしない — React、Vue などではなく、自分のロジックをテストします
  9. テストフィクスチャを維持する — テストデータを現実的で最新の状態に保ちます
  10. テストカバレッジを確認する — カバレッジ 100% でもバグがないとは限りません

よくあるアンチパターン

[X] 実装の詳細をテストする
// BAD - tests internal state
it('sets loading to true', () => {
  const { result } = renderHook(() => useUsers());
  expect(result.current.loading).toBe(true);
});

// GOOD - tests user-visible behavior
it('shows loading spinner while fetching', () => {
  render(<UserList />);
  expect(screen.getByRole('status')).toBeInTheDocument();
});
[X] 過剰なモック
// BAD - mocks everything, tests nothing
vi.mock('./api');
vi.mock('./utils');
vi.mock('./hooks');

// GOOD - mock only external dependencies
vi.mock('axios'); // Mock HTTP client
// Let your code run for real
[X] 壊れやすいセレクター
// BAD - breaks when styling changes
const button = container.querySelector('.btn-primary');

// GOOD - uses accessible queries
const button = screen.getByRole('button', { name: /submit/i });

まとめ

効果的なテストには、次の要素が必要です。

  • 整理されたテストファイル:ソースコードと同じ場所に配置
  • 包括的な単体テスト:ビジネスロジックとユーティリティを対象
  • ユーザー中心のコンポーネントテスト:React Testing Library を使用
  • 現実的な統合テスト:API ルートとデータベース操作を対象
  • エンドツーエンドテスト:重要なユーザーフローを対象
  • API モック:決定的なテストのために MSW を使用
  • 高いカバレッジ目標:新しいコードは 100%、全体で 80% 以上
  • CI との統合:並列実行とカバレッジレポートに対応
  • 不安定なテストの防止:適切なモックと分離によって実現

高精度ツールを使ってテストの不足箇所を見つけ、効率的にテストを実行し、実装の品質を検証してください。

コメント