Files
gh-classmethod-tsumiki/commands/rev-specs.md
2025-11-29 18:09:29 +08:00

623 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
description: 既存のコードベースから包括的なテストケースと仕様書を逆生成します。実装されたビジネスロジック、API動作、UI コンポーネントの動作を分析し、不足しているテストケースを特定・生成し、仕様書として文書化します。
---
# rev-specs
## 目的
既存のコードベースから包括的なテストケースと仕様書を逆生成する。実装されたビジネスロジック、API動作、UI コンポーネントの動作を分析し、不足しているテストケースを特定・生成し、仕様書として文書化する。
## 前提条件
- 分析対象のコードベースが存在する
- `docs/reverse/` ディレクトリが存在する(なければ作成)
- 可能であれば事前に `/tsumiki:rev-requirements`, `/tsumiki:rev-design` を実行済み
## 実行内容
1. **既存テストの分析**
- 単体テストUnit Testの実装状況確認
- 統合テストIntegration Testの実装状況確認
- E2EテストEnd-to-End Testの実装状況確認
- テストカバレッジの測定
2. **実装コードからテストケースの逆生成**
- 関数・メソッドの引数・戻り値からのテストケース生成
- 条件分岐からの境界値テスト生成
- エラーハンドリングからの異常系テスト生成
- データベース操作からのデータテスト生成
3. **API仕様からテストケースの生成**
- 各エンドポイントの正常系テスト
- 認証・認可テスト
- バリデーションエラーテスト
- HTTPステータスコードテスト
4. **UI コンポーネントからテストケースの生成**
- コンポーネントレンダリングテスト
- ユーザーインタラクションテスト
- 状態変更テスト
- プロパティ変更テスト
5. **パフォーマンス・セキュリティテストケースの生成**
- 負荷テストシナリオ
- セキュリティ脆弱性テスト
- レスポンス時間テスト
6. **テスト仕様書の生成**
- テスト計画書
- テストケース一覧
- テスト環境仕様
- テスト手順書
7. **ファイルの作成**
- `docs/reverse/{プロジェクト名}-test-specs.md` - テスト仕様書
- `docs/reverse/{プロジェクト名}-test-cases.md` - テストケース一覧
- `docs/reverse/tests/` - 生成されたテストコード
## 出力フォーマット例
### test-specs.md
```markdown
# {プロジェクト名} テスト仕様書(逆生成)
## 分析概要
**分析日時**: {実行日時}
**対象コードベース**: {パス}
**テストカバレッジ**: {現在のカバレッジ}%
**生成テストケース数**: {生成数}個
**実装推奨テスト数**: {推奨数}個
## 現在のテスト実装状況
### テストフレームワーク
- **単体テスト**: {Jest/Vitest/pytest等}
- **統合テスト**: {Supertest/TestContainers等}
- **E2Eテスト**: {Cypress/Playwright等}
- **コードカバレッジ**: {istanbul/c8等}
### テストカバレッジ詳細
| ファイル/ディレクトリ | 行カバレッジ | 分岐カバレッジ | 関数カバレッジ |
|---------------------|-------------|-------------|-------------|
| src/auth/ | 85% | 75% | 90% |
| src/users/ | 60% | 45% | 70% |
| src/components/ | 40% | 30% | 50% |
| **全体** | **65%** | **55%** | **75%** |
### テストカテゴリ別実装状況
#### 単体テスト
- [x] **認証サービス**: auth.service.spec.ts
- [x] **ユーザーサービス**: user.service.spec.ts
- [ ] **データ変換ユーティリティ**: 未実装
- [ ] **バリデーションヘルパー**: 未実装
#### 統合テスト
- [x] **認証API**: auth.controller.spec.ts
- [ ] **ユーザー管理API**: 未実装
- [ ] **データベース操作**: 未実装
#### E2Eテスト
- [ ] **ユーザーログインフロー**: 未実装
- [ ] **データ操作フロー**: 未実装
- [ ] **エラーハンドリング**: 未実装
## 生成されたテストケース
### API テストケース
#### POST /auth/login - ログイン認証
**正常系テスト**
```typescript
describe('POST /auth/login', () => {
it('有効な認証情報でログイン成功', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.token).toBeDefined();
expect(response.body.data.user.email).toBe('test@example.com');
});
it('JWTトークンが正しい形式で返される', async () => {
const response = await request(app)
.post('/auth/login')
.send(validCredentials);
const token = response.body.data.token;
expect(token).toMatch(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/);
});
});
```
**異常系テスト**
```typescript
describe('POST /auth/login - 異常系', () => {
it('無効なメールアドレスでエラー', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'invalid-email',
password: 'password123'
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
it('存在しないユーザーでエラー', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123'
});
expect(response.status).toBe(401);
expect(response.body.error.code).toBe('INVALID_CREDENTIALS');
});
it('パスワード間違いでエラー', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(response.status).toBe(401);
expect(response.body.error.code).toBe('INVALID_CREDENTIALS');
});
});
```
**境界値テスト**
```typescript
describe('POST /auth/login - 境界値', () => {
it('最小文字数パスワードでテスト', async () => {
// 8文字最小要件
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: '12345678'
});
expect(response.status).toBe(200);
});
it('最大文字数メールアドレスでテスト', async () => {
// 255文字最大要件
const longEmail = 'a'.repeat(243) + '@example.com';
const response = await request(app)
.post('/auth/login')
.send({
email: longEmail,
password: 'password123'
});
expect(response.status).toBe(400);
});
});
```
### UIコンポーネントテストケース
#### LoginForm コンポーネント
**レンダリングテスト**
```typescript
import { render, screen } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('必要な要素が表示される', () => {
render(<LoginForm onSubmit={jest.fn()} />);
expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument();
expect(screen.getByLabelText('パスワード')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'ログイン' })).toBeInTheDocument();
});
it('初期状態でエラーメッセージが非表示', () => {
render(<LoginForm onSubmit={jest.fn()} />);
expect(screen.queryByText(/エラー/)).not.toBeInTheDocument();
});
});
```
**ユーザーインタラクションテスト**
```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm - ユーザーインタラクション', () => {
it('フォーム送信時にonSubmitが呼ばれる', async () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
await userEvent.type(screen.getByLabelText('メールアドレス'), 'test@example.com');
await userEvent.type(screen.getByLabelText('パスワード'), 'password123');
await userEvent.click(screen.getByRole('button', { name: 'ログイン' }));
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
it('バリデーションエラー時に送信されない', async () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
await userEvent.click(screen.getByRole('button', { name: 'ログイン' }));
expect(mockSubmit).not.toHaveBeenCalled();
expect(screen.getByText('メールアドレスは必須です')).toBeInTheDocument();
});
});
```
### サービス層テストケース
#### AuthService 単体テスト
```typescript
import { AuthService } from './auth.service';
import { UserRepository } from './user.repository';
jest.mock('./user.repository');
describe('AuthService', () => {
let authService: AuthService;
let mockUserRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockUserRepository = new UserRepository() as jest.Mocked<UserRepository>;
authService = new AuthService(mockUserRepository);
});
describe('login', () => {
it('有効な認証情報でユーザー情報とトークンを返す', async () => {
const mockUser = {
id: '1',
email: 'test@example.com',
hashedPassword: 'hashed_password'
};
mockUserRepository.findByEmail.mockResolvedValue(mockUser);
jest.spyOn(authService, 'verifyPassword').mockResolvedValue(true);
jest.spyOn(authService, 'generateToken').mockReturnValue('mock_token');
const result = await authService.login('test@example.com', 'password');
expect(result).toEqual({
user: { id: '1', email: 'test@example.com' },
token: 'mock_token'
});
});
it('存在しないユーザーでエラーをスロー', async () => {
mockUserRepository.findByEmail.mockResolvedValue(null);
await expect(
authService.login('nonexistent@example.com', 'password')
).rejects.toThrow('Invalid credentials');
});
});
});
```
## パフォーマンステストケース
### 負荷テスト
```typescript
describe('パフォーマンステスト', () => {
it('ログインAPI - 100同時接続テスト', async () => {
const promises = Array.from({ length: 100 }, () =>
request(app).post('/auth/login').send(validCredentials)
);
const startTime = Date.now();
const responses = await Promise.all(promises);
const endTime = Date.now();
// 全てのリクエストが成功
responses.forEach(response => {
expect(response.status).toBe(200);
});
// 応答時間が5秒以内
expect(endTime - startTime).toBeLessThan(5000);
});
it('データベース - 大量データ検索性能', async () => {
// 1000件のテストデータを作成
await createTestData(1000);
const startTime = Date.now();
const response = await request(app)
.get('/users')
.query({ limit: 100, offset: 0 });
const endTime = Date.now();
expect(response.status).toBe(200);
expect(endTime - startTime).toBeLessThan(1000); // 1秒以内
});
});
```
### セキュリティテスト
```typescript
describe('セキュリティテスト', () => {
it('SQLインジェクション対策', async () => {
const maliciousInput = "'; DROP TABLE users; --";
const response = await request(app)
.post('/auth/login')
.send({
email: maliciousInput,
password: 'password'
});
// システムが正常に動作し、データベースが破損していない
expect(response.status).toBe(400);
// ユーザーテーブルが依然として存在することを確認
const usersResponse = await request(app)
.get('/users')
.set('Authorization', 'Bearer ' + validToken);
expect(usersResponse.status).not.toBe(500);
});
it('XSS対策', async () => {
const xssPayload = '<script>alert("XSS")</script>';
const response = await request(app)
.post('/users')
.set('Authorization', 'Bearer ' + validToken)
.send({
name: xssPayload,
email: 'test@example.com'
});
// レスポンスでスクリプトがエスケープされている
expect(response.body.data.name).not.toContain('<script>');
expect(response.body.data.name).toContain('&lt;script&gt;');
});
});
```
## E2Eテストケース
### Playwright/Cypress テストシナリオ
```typescript
// ユーザーログインフロー E2Eテスト
describe('ユーザーログインフロー', () => {
it('正常なログインからダッシュボード表示まで', async () => {
await page.goto('/login');
// ログインフォーム入力
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
// ダッシュボードへリダイレクト
await page.waitForURL('/dashboard');
// ユーザー情報表示確認
await expect(page.locator('[data-testid="user-name"]')).toContainText('テストユーザー');
// ログアウト機能確認
await page.click('[data-testid="logout-button"]');
await page.waitForURL('/login');
});
it('ログイン失敗時のエラー表示', async () => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'wrong@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
// エラーメッセージ表示確認
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('認証情報が正しくありません');
});
});
```
## テスト環境設定
### データベーステスト設定
```typescript
// テスト用データベース設定
beforeAll(async () => {
// テスト用データベース接続
await setupTestDatabase();
// マイグレーション実行
await runMigrations();
});
beforeEach(async () => {
// 各テスト前にデータをクリーンアップ
await cleanupDatabase();
// 基本テストデータ投入
await seedTestData();
});
afterAll(async () => {
// テスト用データベース切断
await teardownTestDatabase();
});
```
### モック設定
```typescript
// 外部サービスのモック
jest.mock('./email.service', () => ({
EmailService: jest.fn().mockImplementation(() => ({
sendEmail: jest.fn().mockResolvedValue(true)
}))
}));
// 環境変数のモック
process.env.JWT_SECRET = 'test-secret';
process.env.NODE_ENV = 'test';
```
## 不足テストの優先順位
### 高優先度(即座に実装推奨)
1. **E2Eテストスイート** - ユーザーフロー全体の動作保証
2. **API統合テスト** - バックエンドAPI全体のテスト
3. **セキュリティテスト** - 脆弱性対策の検証
### 中優先度(次のスプリントで実装)
1. **パフォーマンステスト** - 負荷・応答時間テスト
2. **UIコンポーネントテスト** - フロントエンド動作保証
3. **データベーステスト** - データ整合性テスト
### 低優先度(継続的改善として実装)
1. **ブラウザ互換性テスト** - 複数ブラウザでの動作確認
2. **アクセシビリティテスト** - a11y対応確認
3. **国際化テスト** - 多言語対応確認
```
### test-cases.md
```markdown
# {プロジェクト名} テストケース一覧(逆生成)
## テストケース概要
| ID | テスト名 | カテゴリ | 優先度 | 実装状況 | 推定工数 |
|----|----------|----------|--------|----------|----------|
| TC-001 | ログイン正常系 | API | 高 | ✅ | 2h |
| TC-002 | ログイン異常系 | API | 高 | ✅ | 3h |
| TC-003 | E2Eログインフロー | E2E | 高 | ❌ | 4h |
| TC-004 | パフォーマンス負荷テスト | パフォーマンス | 中 | ❌ | 6h |
## 詳細テストケース
### TC-001: ログインAPI正常系テスト
**テスト目的**: 有効な認証情報でのログイン機能を検証
**事前条件**:
- テストユーザーがデータベースに存在する
- パスワードが正しくハッシュ化されている
**テスト手順**:
1. POST /auth/login にリクエスト送信
2. 有効なemail, passwordを含むJSONを送信
3. レスポンスを確認
**期待結果**:
- HTTPステータス: 200
- success: true
- data.token: JWT形式のトークン
- data.user: ユーザー情報
**実装ファイル**: `auth.controller.spec.ts`
### TC-002: ログインAPI異常系テスト
**テスト目的**: 無効な認証情報での適切なエラーハンドリングを検証
**テストケース**:
1. 存在しないメールアドレス
2. 無効なパスワード
3. 不正なメール形式
4. 空文字・null値
5. SQLインジェクション攻撃
**期待結果**:
- 適切なHTTPステータスコード
- 統一されたエラーレスポンス形式
- セキュリティ脆弱性がない
**実装状況**: ✅ 部分的実装
```
## テストコード生成アルゴリズム
### 1. 静的解析によるテストケース抽出
```
1. 関数シグネチャ解析 → 引数・戻り値のテストケース
2. 条件分岐解析 → 分岐網羅テストケース
3. 例外処理解析 → 異常系テストケース
4. データベースアクセス解析 → データテストケース
```
### 2. 動的解析によるテスト生成
```
1. API呼び出しログ → 実際の使用パターンテスト
2. ユーザー操作ログ → E2Eテストシナリオ
3. パフォーマンスログ → 負荷テストシナリオ
```
### 3. テストカバレッジギャップ分析
```
1. 現在のカバレッジ測定
2. 未テスト行・分岐の特定
3. クリティカルパスの特定
4. リスクベース優先順位付け
```
## 実行コマンド例
```bash
# フル分析(全テストケース生成)
claude code rev-specs
# 特定のテストカテゴリのみ生成
claude code rev-specs --type unit
claude code rev-specs --type integration
claude code rev-specs --type e2e
# 特定のファイル/ディレクトリを対象
claude code rev-specs --path ./src/auth
# テストコードの実際の生成と出力
claude code rev-specs --generate-code
# カバレッジレポートと合わせて分析
claude code rev-specs --with-coverage
# 優先度フィルタリング
claude code rev-specs --priority high
```
## 実行後の確認
- 現在のテストカバレッジと不足部分の詳細レポート表示
- 生成されたテストケース数と推定実装工数を表示
- 優先順位付けされた実装推奨リストを提示
- テスト環境の設定要件と推奨ツールを提案
- CI/CD パイプラインへの統合案を提示