Claude Code Plugins

Community-maintained marketplace

Feedback

test-generator

@ntaksh42/agents
0
0

Generate comprehensive unit, integration, and E2E tests with edge cases, mocks, and assertions. Use when writing tests for functions, classes, APIs, or implementing TDD.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name test-generator
description Generate comprehensive unit, integration, and E2E tests with edge cases, mocks, and assertions. Use when writing tests for functions, classes, APIs, or implementing TDD.

Test Generator Skill

包括的なテストコードを自動生成するスキルです。

概要

このスキルは、ソースコードから高品質なユニットテスト、統合テスト、E2Eテストを自動生成します。テスト駆動開発(TDD)をサポートし、エッジケース、モック、アサーションを適切に含んだテストコードを作成します。

主な機能

  • ユニットテスト生成: 関数・メソッド単位の詳細なテスト
  • 統合テスト生成: モジュール間の連携テスト
  • E2Eテスト生成: エンドツーエンドのシナリオテスト
  • エッジケース網羅: 境界値、null、例外ケース等を自動検出
  • モック生成: 外部依存のモックオブジェクト作成
  • アサーション充実: 期待値と実際の値の包括的な検証
  • テストデータ生成: リアルなテストデータとフィクスチャ
  • カバレッジ最適化: 高いコードカバレッジを達成
  • AAA パターン: Arrange-Act-Assert の明確な構造
  • ドキュメント: テストの目的と意図を説明するコメント

サポートフレームワーク

JavaScript/TypeScript

  • Jest: React, Node.js の標準
  • Vitest: Vite ベースの高速テスト
  • Mocha + Chai: 柔軟な設定
  • Jasmine: Angular の標準
  • Cypress: E2E テスト
  • Playwright: クロスブラウザテスト
  • Testing Library: React Testing Library, Vue Testing Library

Python

  • pytest: モダンで強力
  • unittest: 標準ライブラリ
  • doctest: ドキュメント内テスト
  • nose2: 拡張テストランナー
  • Robot Framework: キーワード駆動テスト

Java

  • JUnit 5: 最新標準
  • TestNG: 強力な設定
  • Mockito: モックフレームワーク
  • AssertJ: 流暢なアサーション
  • Spring Test: Spring Boot テスト

Go

  • testing: 標準ライブラリ
  • testify: アサーションとモック
  • ginkgo: BDD スタイル
  • gomock: モックジェネレーター

その他

  • Rust: cargo test
  • C#: xUnit, NUnit, MSTest
  • Ruby: RSpec, Minitest
  • PHP: PHPUnit
  • Swift: XCTest

使用方法

基本的なテスト生成

この関数のテストを生成してください:

function calculateTotal(items, taxRate) {
  return items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

フレームワーク: Jest

クラス全体のテスト

このクラスの包括的なテストスイートを作成:
- すべてのメソッドをカバー
- エッジケースを含む
- モックを適切に使用

[クラスコード]

APIエンドポイントのテスト

以下のAPIエンドポイントのテストを生成:

POST /api/users
- 成功ケース
- バリデーションエラー
- 認証エラー
- データベースエラー

TDD スタイル

以下の要件からテストを先に生成(TDD):

要件:
- ユーザー登録機能
- メールアドレスの検証
- パスワードのハッシュ化
- 重複登録の防止

フレームワーク: pytest

テスト生成パターン

1. ユニットテスト

入力:

export function formatCurrency(amount: number, currency: string = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency
  }).format(amount);
}

生成されるテスト:

import { formatCurrency } from './currency';

describe('formatCurrency', () => {
  describe('正常系', () => {
    it('デフォルトでUSD形式でフォーマットする', () => {
      const result = formatCurrency(1234.56);
      expect(result).toBe('$1,234.56');
    });

    it('指定された通貨でフォーマットする', () => {
      const result = formatCurrency(1234.56, 'EUR');
      expect(result).toBe('€1,234.56');
    });

    it('整数もフォーマットする', () => {
      const result = formatCurrency(1000);
      expect(result).toBe('$1,000.00');
    });

    it('小数点以下を適切に処理する', () => {
      const result = formatCurrency(1234.567);
      expect(result).toBe('$1,234.57'); // 四捨五入
    });
  });

  describe('エッジケース', () => {
    it('0をフォーマットする', () => {
      const result = formatCurrency(0);
      expect(result).toBe('$0.00');
    });

    it('負の数をフォーマットする', () => {
      const result = formatCurrency(-1234.56);
      expect(result).toBe('-$1,234.56');
    });

    it('非常に大きな数をフォーマットする', () => {
      const result = formatCurrency(1234567890.12);
      expect(result).toBe('$1,234,567,890.12');
    });

    it('非常に小さな数をフォーマットする', () => {
      const result = formatCurrency(0.01);
      expect(result).toBe('$0.01');
    });
  });

  describe('異常系', () => {
    it('無効な通貨コードでエラーをスローする', () => {
      expect(() => formatCurrency(100, 'INVALID')).toThrow();
    });

    it('NaNを適切に処理する', () => {
      expect(() => formatCurrency(NaN)).toThrow();
    });

    it('Infinityを適切に処理する', () => {
      expect(() => formatCurrency(Infinity)).toThrow();
    });
  });
});

2. 統合テスト(API)

入力:

@app.route('/api/orders', methods=['POST'])
def create_order():
    data = request.json
    user_id = get_current_user_id()
    order = Order.create(user_id, data['items'])
    return jsonify(order.to_dict()), 201

生成されるテスト:

import pytest
from app import app, db
from models import User, Order, OrderItem

@pytest.fixture
def client():
    app.config['TESTING'] = True
    with app.test_client() as client:
        with app.app_context():
            db.create_all()
        yield client
        with app.app_context():
            db.drop_all()

@pytest.fixture
def auth_headers(client):
    """認証済みユーザーのヘッダー"""
    user = User.create(email='test@example.com', password='password123')
    token = user.generate_token()
    return {'Authorization': f'Bearer {token}'}

class TestCreateOrder:
    """POST /api/orders のテスト"""

    def test_正常に注文を作成できる(self, client, auth_headers):
        """正常系: 有効なデータで注文を作成"""
        # Arrange
        order_data = {
            'items': [
                {'product_id': 1, 'quantity': 2, 'price': 1000},
                {'product_id': 2, 'quantity': 1, 'price': 2000}
            ]
        }

        # Act
        response = client.post(
            '/api/orders',
            json=order_data,
            headers=auth_headers
        )

        # Assert
        assert response.status_code == 201
        data = response.json
        assert 'id' in data
        assert 'created_at' in data
        assert len(data['items']) == 2
        assert data['total'] == 4000

    def test_認証なしで401エラー(self, client):
        """異常系: 認証ヘッダーなし"""
        response = client.post('/api/orders', json={'items': []})
        assert response.status_code == 401

    def test_空の注文でバリデーションエラー(self, client, auth_headers):
        """異常系: 注文アイテムが空"""
        response = client.post(
            '/api/orders',
            json={'items': []},
            headers=auth_headers
        )
        assert response.status_code == 400
        assert 'items' in response.json['errors']

    def test_無効な商品IDでエラー(self, client, auth_headers):
        """異常系: 存在しない商品ID"""
        order_data = {
            'items': [{'product_id': 99999, 'quantity': 1, 'price': 100}]
        }
        response = client.post(
            '/api/orders',
            json=order_data,
            headers=auth_headers
        )
        assert response.status_code == 404

    def test_負の数量でバリデーションエラー(self, client, auth_headers):
        """エッジケース: 負の数量"""
        order_data = {
            'items': [{'product_id': 1, 'quantity': -1, 'price': 100}]
        }
        response = client.post(
            '/api/orders',
            json=order_data,
            headers=auth_headers
        )
        assert response.status_code == 400

    def test_在庫不足の場合のエラー(self, client, auth_headers):
        """ビジネスロジック: 在庫不足"""
        order_data = {
            'items': [{'product_id': 1, 'quantity': 1000, 'price': 100}]
        }
        response = client.post(
            '/api/orders',
            json=order_data,
            headers=auth_headers
        )
        assert response.status_code == 409
        assert 'stock' in response.json['message'].lower()

    def test_データベースエラーの処理(self, client, auth_headers, monkeypatch):
        """エラーハンドリング: DB接続エラー"""
        def mock_create(*args, **kwargs):
            raise DatabaseError("Connection failed")

        monkeypatch.setattr(Order, 'create', mock_create)

        response = client.post(
            '/api/orders',
            json={'items': [{'product_id': 1, 'quantity': 1, 'price': 100}]},
            headers=auth_headers
        )
        assert response.status_code == 500

3. E2Eテスト

入力:

ユーザー登録からログインまでのE2Eテストを生成:
フレームワーク: Playwright

生成されるテスト:

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

test.describe('ユーザー認証フロー', () => {
  test('新規ユーザー登録からログインまで', async ({ page }) => {
    // 1. 登録ページに移動
    await page.goto('/signup');
    await expect(page).toHaveTitle(/Sign Up/);

    // 2. 登録フォームに入力
    const timestamp = Date.now();
    const email = `test${timestamp}@example.com`;
    const password = 'SecurePassword123!';

    await page.fill('input[name="email"]', email);
    await page.fill('input[name="password"]', password);
    await page.fill('input[name="confirmPassword"]', password);
    await page.fill('input[name="name"]', 'Test User');

    // 3. 登録を実行
    await page.click('button[type="submit"]');

    // 4. 成功メッセージを確認
    await expect(page.locator('.success-message')).toBeVisible();
    await expect(page.locator('.success-message')).toContainText(
      'Registration successful'
    );

    // 5. 確認メールの検証(モック)
    // 実際の実装ではメールサービスのAPIをチェック

    // 6. ログインページに移動
    await page.goto('/login');

    // 7. ログイン情報を入力
    await page.fill('input[name="email"]', email);
    await page.fill('input[name="password"]', password);
    await page.click('button[type="submit"]');

    // 8. ダッシュボードにリダイレクトされることを確認
    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.locator('.user-name')).toContainText('Test User');

    // 9. ユーザーメニューが表示されることを確認
    await page.click('.user-menu-toggle');
    await expect(page.locator('.user-menu')).toBeVisible();
    await expect(page.locator('.user-email')).toContainText(email);
  });

  test('無効なメールアドレスでエラー表示', async ({ page }) => {
    await page.goto('/signup');

    await page.fill('input[name="email"]', 'invalid-email');
    await page.fill('input[name="password"]', 'Password123!');
    await page.click('button[type="submit"]');

    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).toContainText(
      'Invalid email address'
    );
  });

  test('既存メールアドレスで登録エラー', async ({ page }) => {
    await page.goto('/signup');

    // 既に存在するメールアドレスを使用
    await page.fill('input[name="email"]', 'existing@example.com');
    await page.fill('input[name="password"]', 'Password123!');
    await page.fill('input[name="confirmPassword"]', 'Password123!');
    await page.click('button[type="submit"]');

    await expect(page.locator('.error-message')).toContainText(
      'Email already registered'
    );
  });
});

4. モックの生成

入力:

public class UserService {
    private UserRepository repository;
    private EmailService emailService;

    public User createUser(UserDto dto) {
        User user = repository.save(new User(dto));
        emailService.sendWelcomeEmail(user.getEmail());
        return user;
    }
}

生成されるテスト:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository repository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_正常系_ユーザーを作成しウェルカムメールを送信() {
        // Arrange
        UserDto dto = new UserDto("test@example.com", "John Doe");
        User savedUser = new User(1L, "test@example.com", "John Doe");

        when(repository.save(any(User.class))).thenReturn(savedUser);
        doNothing().when(emailService).sendWelcomeEmail(anyString());

        // Act
        User result = userService.createUser(dto);

        // Assert
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("test@example.com", result.getEmail());
        assertEquals("John Doe", result.getName());

        // Verify interactions
        verify(repository, times(1)).save(any(User.class));
        verify(emailService, times(1)).sendWelcomeEmail("test@example.com");
    }

    @Test
    void createUser_異常系_リポジトリエラーでメール送信しない() {
        // Arrange
        UserDto dto = new UserDto("test@example.com", "John Doe");
        when(repository.save(any(User.class)))
            .thenThrow(new DatabaseException("Connection failed"));

        // Act & Assert
        assertThrows(DatabaseException.class, () -> {
            userService.createUser(dto);
        });

        // メールサービスが呼ばれないことを確認
        verify(emailService, never()).sendWelcomeEmail(anyString());
    }

    @Test
    void createUser_異常系_メール送信失敗時の処理() {
        // Arrange
        UserDto dto = new UserDto("test@example.com", "John Doe");
        User savedUser = new User(1L, "test@example.com", "John Doe");

        when(repository.save(any(User.class))).thenReturn(savedUser);
        doThrow(new EmailException("SMTP error"))
            .when(emailService).sendWelcomeEmail(anyString());

        // Act & Assert
        // メール送信失敗でもユーザーは作成される想定
        User result = userService.createUser(dto);
        assertNotNull(result);

        // または、メール送信失敗時に例外をスローする実装の場合
        // assertThrows(EmailException.class, () -> userService.createUser(dto));
    }
}

テスト戦略

テストピラミッド

        /\
       /E2E\       少数: 重要なユーザーフロー
      /------\
     /統合テスト\     中程度: モジュール間連携
    /----------\
   /ユニットテスト\   多数: 個別関数・メソッド
  /--------------\

カバレッジ目標

  • ユニットテスト: 80-90% のコードカバレッジ
  • 統合テスト: 主要なエンドポイント、モジュール連携
  • E2Eテスト: クリティカルなユーザージャーニー

テストデータ生成

ファクトリーパターン

# pytest fixture
@pytest.fixture
def user_factory():
    """ユーザーテストデータのファクトリー"""
    def _create_user(**kwargs):
        defaults = {
            'email': f'user{random.randint(1000, 9999)}@example.com',
            'name': 'Test User',
            'age': 25,
            'active': True
        }
        defaults.update(kwargs)
        return User(**defaults)
    return _create_user

def test_user_creation(user_factory):
    user1 = user_factory()
    user2 = user_factory(email='specific@example.com', age=30)
    assert user1.email != user2.email
    assert user2.age == 30

フィクスチャ

// Jest fixture
export const testUsers = {
  admin: {
    id: 1,
    email: 'admin@example.com',
    role: 'admin',
    permissions: ['read', 'write', 'delete']
  },
  regularUser: {
    id: 2,
    email: 'user@example.com',
    role: 'user',
    permissions: ['read']
  },
  inactiveUser: {
    id: 3,
    email: 'inactive@example.com',
    role: 'user',
    active: false
  }
};

ベストプラクティス

1. テストの独立性

// ❌ 悪い例: テスト間で状態を共有
let sharedUser;

test('create user', () => {
  sharedUser = createUser();
});

test('update user', () => {
  updateUser(sharedUser); // 前のテストに依存
});

// ✅ 良い例: 各テストが独立
test('create user', () => {
  const user = createUser();
  expect(user).toBeDefined();
});

test('update user', () => {
  const user = createUser(); // 独自にセットアップ
  updateUser(user);
  expect(user.updatedAt).toBeDefined();
});

2. 明確なテスト名

# ❌ 悪い例
def test_user():
    pass

# ✅ 良い例
def test_create_user_with_valid_email_succeeds():
    pass

def test_create_user_with_invalid_email_raises_validation_error():
    pass

3. AAA パターン

test('calculateDiscount applies 10% discount for premium users', () => {
  // Arrange (準備)
  const user = { type: 'premium' };
  const price = 1000;

  // Act (実行)
  const result = calculateDiscount(user, price);

  // Assert (検証)
  expect(result).toBe(900);
});

4. 適切なアサーション

// ❌ 悪い例: 曖昧なアサーション
expect(result).toBeTruthy();

// ✅ 良い例: 具体的なアサーション
expect(result).toBe(true);
expect(result.status).toBe('success');
expect(result.data).toHaveLength(5);
expect(result.error).toBeUndefined();

カスタマイズオプション

テスト生成の詳細設定

以下の設定でテストを生成:

- フレームワーク: Jest
- カバレッジ目標: 90%
- エッジケース: 徹底的に
- モック: 外部API、データベース
- アサーションスタイル: expect
- ファイル命名: *.test.ts
- テストデータ: ファクトリーパターン使用

統合とCI/CD

GitHub Actions

name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npm test
      - run: npm run test:coverage
      - uses: codecov/codecov-action@v3

制限事項

  • ビジネスロジックの理解: 要件の深い理解が必要なテストは人間の補完が必要
  • 外部依存: 実際の外部サービスとの統合テストは設定が必要
  • UI/UXテスト: ビジュアル回帰テストは専用ツールが必要
  • パフォーマンステスト: 負荷テストは別のツールが推奨

バージョン情報

  • スキルバージョン: 1.0.0
  • 最終更新: 2025-01-22

使用例:

この関数の包括的なテストスイートを生成してください:

function validatePassword(password) {
  if (password.length < 8) return false;
  if (!/[A-Z]/.test(password)) return false;
  if (!/[a-z]/.test(password)) return false;
  if (!/[0-9]/.test(password)) return false;
  if (!/[!@#$%^&*]/.test(password)) return false;
  return true;
}

要件:
- フレームワーク: Jest
- エッジケースを網羅
- 各条件の境界値テスト
- わかりやすいテスト名

このプロンプトで、完全なテストスイートが生成されます!