---
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('