AgentSkillsCN

test-generator

自动生成并执行单元测试、集成测试以及端到端测试。 适用于测试用例编写、TDD 测试方法、覆盖率提升,以及在修复 Bug 后进行回归测试。 支持 Jest、Vitest、React Testing Library 和 Playwright 等主流测试框架。 触发关键词:“测试”、“test”、“覆盖率”。

SKILL.md
--- frontmatter
name: test-generator
description: |
  ユニットテスト・統合テスト・E2Eテストを自動生成し実行。
  テスト作成、TDD、カバレッジ向上、バグ修正後の回帰テスト時に使用。
  Jest、Vitest、React Testing Library、Playwright対応。
  「テスト」「test」「カバレッジ」で発動。
allowed-tools: Read, Write, Bash

テスト生成スキル

テストピラミッド戦略

テストタイプ目安数/機能ツール目的
Unit Tests5-15個Vitest/Jestビジネスロジック、エッジケース
Integration Tests3-8個RTLAPI契約、コンポーネント連携
E2E Tests2-5個Playwrightクリティカルユーザージャーニー

カバレッジ目標

  • Statements: 80%以上
  • Branches: 75%以上
  • Functions: 80%以上
  • Lines: 80%以上

テスト命名規則

typescript
describe('対象モジュール', () => {
  describe('対象メソッド/機能', () => {
    it('〜の場合、〜となること', () => {
      // Arrange(準備)
      // Act(実行)
      // Assert(検証)
    });
  });
});

実行手順

  1. 対象コードの分析

    • 関数のシグネチャ、入出力を把握
    • エッジケース(null、空配列、境界値)を特定
    • 外部依存(API、DB)を確認
  2. テストファイル生成

    • 配置: __tests__/ または *.test.ts
    • 命名: [対象ファイル名].test.ts
  3. テスト実行

    bash
    # ユニット/統合テスト
    npm run test -- --coverage
    
    # 特定ファイルのみ
    npm run test -- [ファイル名]
    
    # E2Eテスト
    npx playwright test
    
  4. 結果確認と修正

    • 失敗したテストがあれば原因を分析
    • 実装コードまたはテストコードを修正
    • 全テストがパスするまでループ

テストパターンテンプレート

ユニットテスト(純粋関数)

typescript
import { describe, it, expect, vi } from 'vitest';
import { calculateTotal, formatCurrency } from './utils';

describe('calculateTotal', () => {
  // 正常系
  it('数値配列の合計を正しく計算すること', () => {
    expect(calculateTotal([100, 200, 300])).toBe(600);
  });

  // 境界値
  it('空配列の場合0を返すこと', () => {
    expect(calculateTotal([])).toBe(0);
  });

  it('1要素の配列でもその値を返すこと', () => {
    expect(calculateTotal([42])).toBe(42);
  });

  // 異常系
  it('不正な入力でエラーをスローすること', () => {
    expect(() => calculateTotal(null as any)).toThrow('Invalid input');
  });

  it('負の数も正しく計算すること', () => {
    expect(calculateTotal([-100, 50])).toBe(-50);
  });
});

非同期関数テスト

typescript
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './api';

describe('fetchUser', () => {
  it('ユーザー情報を正しく取得すること', async () => {
    const user = await fetchUser(1);
    
    expect(user).toMatchObject({
      id: 1,
      name: expect.any(String),
      email: expect.stringContaining('@'),
    });
  });

  it('存在しないユーザーでエラーをスローすること', async () => {
    await expect(fetchUser(99999)).rejects.toThrow('User not found');
  });
});

コンポーネントテスト(React Testing Library)

typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = vi.fn();

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('フォームが正しくレンダリングされること', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument();
    expect(screen.getByLabelText('パスワード')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'ログイン' })).toBeInTheDocument();
  });

  it('有効な入力で送信できること', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByLabelText('メールアドレス'), 'test@example.com');
    await user.type(screen.getByLabelText('パスワード'), 'password123');
    await user.click(screen.getByRole('button', { name: 'ログイン' }));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });
  });

  it('無効なメールでエラーが表示されること', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByLabelText('メールアドレス'), 'invalid-email');
    await user.click(screen.getByRole('button', { name: 'ログイン' }));

    expect(await screen.findByRole('alert')).toHaveTextContent('有効なメールアドレス');
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('送信中はボタンが無効化されること', async () => {
    const user = userEvent.setup();
    mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1000)));
    
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByLabelText('メールアドレス'), 'test@example.com');
    await user.type(screen.getByLabelText('パスワード'), 'password123');
    await user.click(screen.getByRole('button', { name: 'ログイン' }));

    expect(screen.getByRole('button')).toBeDisabled();
    expect(screen.getByRole('button')).toHaveTextContent('ログイン中');
  });
});

APIルートテスト(Next.js)

typescript
import { createMocks } from 'node-mocks-http';
import { POST } from '@/app/api/users/route';

describe('POST /api/users', () => {
  it('ユーザーを正常に作成できること', async () => {
    const { req } = createMocks({
      method: 'POST',
      body: {
        email: 'new@example.com',
        name: 'New User',
        password: 'securePassword123',
      },
    });

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

    expect(response.status).toBe(201);
    expect(data.success).toBe(true);
    expect(data.data).toMatchObject({
      email: 'new@example.com',
      name: 'New User',
    });
  });

  it('メール重複で409エラーを返すこと', async () => {
    const { req } = createMocks({
      method: 'POST',
      body: {
        email: 'existing@example.com',
        name: 'Test',
        password: 'password123',
      },
    });

    const response = await POST(req);
    
    expect(response.status).toBe(409);
  });
});

E2Eテスト(Playwright)

typescript
import { test, expect } from '@playwright/test';

test.describe('ログインフロー', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('正常にログインできること', async ({ page }) => {
    await page.getByLabel('メールアドレス').fill('user@example.com');
    await page.getByLabel('パスワード').fill('password123');
    await page.getByRole('button', { name: 'ログイン' }).click();

    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByRole('heading', { name: 'ダッシュボード' })).toBeVisible();
  });

  test('誤ったパスワードでエラーが表示されること', async ({ page }) => {
    await page.getByLabel('メールアドレス').fill('user@example.com');
    await page.getByLabel('パスワード').fill('wrongpassword');
    await page.getByRole('button', { name: 'ログイン' }).click();

    await expect(page.getByText('メールアドレスまたはパスワードが正しくありません')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('未入力で送信するとバリデーションエラーが表示されること', async ({ page }) => {
    await page.getByRole('button', { name: 'ログイン' }).click();

    await expect(page.getByText('メールアドレスを入力してください')).toBeVisible();
    await expect(page.getByText('パスワードを入力してください')).toBeVisible();
  });
});

モック・スタブパターン

API呼び出しのモック

typescript
import { vi } from 'vitest';

// fetch全体をモック
global.fetch = vi.fn();

beforeEach(() => {
  (fetch as any).mockResolvedValue({
    ok: true,
    json: async () => ({ data: 'mocked' }),
  });
});

外部モジュールのモック

typescript
vi.mock('@/lib/auth', () => ({
  getCurrentUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test User' }),
  signOut: vi.fn(),
}));

出力フォーマット

markdown
## テスト生成結果

### 生成されたテストファイル
- `src/utils/__tests__/calculateTotal.test.ts`
- `src/components/__tests__/LoginForm.test.tsx`

### テスト実行結果
✅ Test Suites: 2 passed, 2 total
✅ Tests: 12 passed, 12 total
⏱️ Time: 3.45s

### カバレッジ
| ファイル | Stmts | Branch | Funcs | Lines |
|---------|-------|--------|-------|-------|
| utils.ts | 95% | 90% | 100% | 95% |
| LoginForm.tsx | 88% | 75% | 85% | 88% |

生成前の確認事項

  1. テストの種類(Unit/Integration/E2E)は?
  2. モックが必要な外部依存は?
  3. 特に重視するエッジケースは?