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
テスト生成パターン
- ユニットテスト
入力:
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();
});
}); });
- 統合テスト(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'
);
}); });
- モックの生成
入力:
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 } };
ベストプラクティス
- テストの独立性
// ❌ 悪い例: テスト間で状態を共有 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(); });
- 明確なテスト名
❌ 悪い例
def test_user(): pass
✅ 良い例
def test_create_user_with_valid_email_succeeds(): pass
def test_create_user_with_invalid_email_raises_validation_error(): pass
- 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); });
- 適切なアサーション
// ❌ 悪い例: 曖昧なアサーション 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
- エッジケースを網羅
- 各条件の境界値テスト
- わかりやすいテスト名
このプロンプトで、完全なテストスイートが生成されます!