--- 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(); expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument(); expect(screen.getByLabelText('パスワード')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'ログイン' })).toBeInTheDocument(); }); it('初期状態でエラーメッセージが非表示', () => { render(); 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(); 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(); 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; beforeEach(() => { mockUserRepository = new UserRepository() as jest.Mocked; 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 = ''; 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('