skills/ntaksh42/agents/test-generator

test-generator

SKILL.md

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
- エッジケースを網羅
- 各条件の境界値テスト
- わかりやすいテスト名

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

Weekly Installs
5
Repository
ntaksh42/agents
GitHub Stars
1
First Seen
Jan 29, 2026
Installed on
claude-code5
opencode4
github-copilot4
codex4
kimi-cli4
gemini-cli4