|
| 1 | +# ACLPracticeProvider 単体テスト追加計画 |
| 2 | + |
| 3 | +**作成日**: 2025-12-17 |
| 4 | + |
| 5 | +**対象ブランチ**: #2962 |
| 6 | + |
| 7 | +**優先度**: High |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## 概要 |
| 12 | + |
| 13 | +`ACLPracticeProvider`(AtCoder Library Practice Contest)に対する単体テストを追加する計画。 |
| 14 | + |
| 15 | +**対象ファイル**: |
| 16 | + |
| 17 | +- **Provider実装**: [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) |
| 18 | + - `ACLPracticeProvider` (773行目~) |
| 19 | +- **テストファイル**: [`src/test/lib/utils/contest_table_provider.test.ts`](../../../../../src/test/lib/utils/contest_table_provider.test.ts) |
| 20 | +- **テストケースファイル**: [`src/test/lib/utils/test_cases/contest_table_provider.ts`](../../../../../src/test/lib/utils/test_cases/contest_table_provider.ts) |
| 21 | + |
| 22 | +**参照ドキュメント**: |
| 23 | + |
| 24 | +- [`docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md`](../../2025-12-11/add_tests_for_contest_table_provider/plan.md) - ABSProvider テスト設計パターンの参考 |
| 25 | +- [`prisma/tasks.ts`](../../../../../prisma/tasks.ts) - practice2 及び各問題のタスク定義 |
| 26 | +- [`src/lib/utils/contest.ts`](../../../../../src/lib/utils/contest.ts) - contest_id 'practice2' → ContestType.ACL_PRACTICE の判別ロジック |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## ACLPracticeProvider の仕様と特徴 |
| 31 | + |
| 32 | +### 基本情報 |
| 33 | + |
| 34 | +- **名称**: AtCoder Library Practice Contest |
| 35 | +- **contest_id**: `'practice2'` |
| 36 | +- **ContestType**: `ContestType.ACL_PRACTICE` |
| 37 | + |
| 38 | +### 問題構成 |
| 39 | + |
| 40 | +ACL Practice は高度なアルゴリズム技法を学ぶための教育的コンテンツで、**12問**で構成されている: |
| 41 | + |
| 42 | +| 問題番号 | task_id | 難易度 | problem_index | |
| 43 | +| -------- | ----------- | ------ | ------------- | |
| 44 | +| 1 | practice2_a | Q3 | A | |
| 45 | +| 2 | practice2_b | Q1 | B | |
| 46 | +| 3 | practice2_c | D2 | C | |
| 47 | +| 4 | practice2_d | D2 | D | |
| 48 | +| 5 | practice2_e | D3 | E | |
| 49 | +| 6 | practice2_f | D2 | F | |
| 50 | +| 7 | practice2_g | D2 | G | |
| 51 | +| 8 | practice2_h | D2 | H | |
| 52 | +| 9 | practice2_i | D2 | I | |
| 53 | +| 10 | practice2_j | D1 | J | |
| 54 | +| 11 | practice2_k | D2 | K | |
| 55 | +| 12 | practice2_l | D2 | L | |
| 56 | + |
| 57 | +**重要な特徴**: |
| 58 | + |
| 59 | +- **単一コンテスト由来**: すべての問題が同じコンテスト 'practice2' に属する(複数コンテスト由来の問題はない) |
| 60 | +- **難易度順**: A~L の順序が段階的難易度を表している |
| 61 | +- **contest_idの統一**: すべての問題のcontest_idは'practice2'で統一される |
| 62 | + |
| 63 | +### ディスプレイ設定 |
| 64 | + |
| 65 | +ACLPracticeProviderは EDPCProvider と同じ設定を持つ: |
| 66 | + |
| 67 | +```typescript |
| 68 | +{ |
| 69 | + isShownHeader: false, // ヘッダーを非表示 |
| 70 | + isShownRoundLabel: false, // ラウンドラベルを非表示 |
| 71 | + isShownTaskIndex: true, // タスクインデックスを表示 |
| 72 | + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', |
| 73 | + roundLabelWidth: '', // ラウンドラベル幅なし |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +## テスト設計 |
| 80 | + |
| 81 | +### テストファイル配置 |
| 82 | + |
| 83 | +**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` |
| 84 | + |
| 85 | +**配置**: 「JOI First Qual Round provider」セクションの直前(1981行目付近) |
| 86 | + |
| 87 | +### テストデータ構築 |
| 88 | + |
| 89 | +#### テストケースファイルでの準備 |
| 90 | + |
| 91 | +`src/test/lib/utils/test_cases/contest_table_provider.ts`に以下を追加: |
| 92 | + |
| 93 | +```typescript |
| 94 | +/** |
| 95 | + * Test data for ACLPracticeProvider (AtCoder Library Practice Contest) |
| 96 | + * 12 problems with progressive difficulty, problem_index from A to L |
| 97 | + * Test data includes varied submission statuses: |
| 98 | + */ |
| 99 | +export const taskResultsForACLPracticeProvider: TaskResults = [ |
| 100 | + createContestTasksForACLPractice('practice2_a', 'practice2', 'A', AC), |
| 101 | + createContestTasksForACLPractice('practice2_b', 'practice2', 'B', AC), |
| 102 | + createContestTasksForACLPractice('practice2_c', 'practice2', 'C', AC_WITH_EDITORIAL), |
| 103 | + createContestTasksForACLPractice('practice2_d', 'practice2', 'D', AC_WITH_EDITORIAL), |
| 104 | + createContestTasksForACLPractice('practice2_e', 'practice2', 'E', TRYING), |
| 105 | + createContestTasksForACLPractice('practice2_f', 'practice2', 'F', AC_WITH_EDITORIAL), |
| 106 | + createContestTasksForACLPractice('practice2_g', 'practice2', 'G', AC_WITH_EDITORIAL), |
| 107 | + createContestTasksForACLPractice('practice2_h', 'practice2', 'H', TRYING), |
| 108 | + createContestTasksForACLPractice('practice2_i', 'practice2', 'I', TRYING), |
| 109 | + createContestTasksForACLPractice('practice2_j', 'practice2', 'J', AC), |
| 110 | + createContestTasksForACLPractice('practice2_k', 'practice2', 'K', PENDING), |
| 111 | + createContestTasksForACLPractice('practice2_l', 'practice2', 'L', AC_WITH_EDITORIAL), |
| 112 | +]; |
| 113 | + |
| 114 | +function createContestTasksForACLPractice( |
| 115 | + taskId: string, |
| 116 | + contestId: string, |
| 117 | + taskTableIndex: string, |
| 118 | + statusName: string, |
| 119 | +): TaskResult { |
| 120 | + return createTaskResultWithTaskTableIndex(contestId, taskId, taskTableIndex, statusName); |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### テストケース詳細 |
| 125 | + |
| 126 | +#### テスト1.1: フィルタリング(contest_id検証) |
| 127 | + |
| 128 | +contest_id='practice2' のタスクのみをフィルタリングすることを確認。 |
| 129 | + |
| 130 | +```typescript |
| 131 | +test('expects to filter tasks with contest_id "practice2"', () => { |
| 132 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 133 | + const mixed = [ |
| 134 | + { contest_id: 'practice2', task_id: 'practice2_a', task_table_index: 'A' }, |
| 135 | + { contest_id: 'practice2', task_id: 'practice2_l', task_table_index: 'L' }, |
| 136 | + { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, |
| 137 | + { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, |
| 138 | + ]; |
| 139 | + |
| 140 | + const filtered = provider.filter(mixed as TaskResults); |
| 141 | + |
| 142 | + expect(filtered).toHaveLength(2); |
| 143 | + expect(filtered.every((task) => task.contest_id === 'practice2')).toBe(true); |
| 144 | +}); |
| 145 | +``` |
| 146 | + |
| 147 | +#### テスト1.2: コンテストタイプ判別 |
| 148 | + |
| 149 | +ContestType.ACL_PRACTICE のみをフィルタリングすることを確認。 |
| 150 | + |
| 151 | +```typescript |
| 152 | +test('expects to filter only ACL_PRACTICE-type contests', () => { |
| 153 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 154 | + const mixed = [ |
| 155 | + { contest_id: 'practice2', task_id: 'practice2_a', task_table_index: 'A' }, |
| 156 | + { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, |
| 157 | + { contest_id: 'abc378', task_id: 'abc378_a', task_table_index: 'A' }, |
| 158 | + ]; |
| 159 | + |
| 160 | + const filtered = provider.filter(mixed as TaskResults); |
| 161 | + |
| 162 | + expect(filtered).toHaveLength(1); |
| 163 | + expect(filtered[0].contest_id).toBe('practice2'); |
| 164 | +}); |
| 165 | +``` |
| 166 | + |
| 167 | +#### テスト1.3: メタデータ取得 |
| 168 | + |
| 169 | +```typescript |
| 170 | +test('expects to return correct metadata', () => { |
| 171 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 172 | + const metadata = provider.getMetadata(); |
| 173 | + |
| 174 | + expect(metadata.title).toBe('AtCoder Library Practice Contest'); |
| 175 | + expect(metadata.abbreviationName).toBe('aclPractice'); |
| 176 | +}); |
| 177 | +``` |
| 178 | + |
| 179 | +#### テスト1.4: ディスプレイ設定確認 |
| 180 | + |
| 181 | +ディスプレイ設定が ACL Practice 固有の値であることを確認。 |
| 182 | + |
| 183 | +```typescript |
| 184 | +test('expects to return correct display config with ACL Practice-specific settings', () => { |
| 185 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 186 | + const config = provider.getDisplayConfig(); |
| 187 | + |
| 188 | + expect(config.isShownHeader).toBe(false); |
| 189 | + expect(config.isShownRoundLabel).toBe(false); |
| 190 | + expect(config.isShownTaskIndex).toBe(true); |
| 191 | + expect(config.tableBodyCellsWidth).toBe( |
| 192 | + 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', |
| 193 | + ); |
| 194 | + expect(config.roundLabelWidth).toBe(''); |
| 195 | +}); |
| 196 | +``` |
| 197 | + |
| 198 | +#### テスト1.5: ラウンドラベルフォーマット |
| 199 | + |
| 200 | +ACL Practice ではラウンドラベルが空文字列で返されることを確認。 |
| 201 | + |
| 202 | +```typescript |
| 203 | +test('expects to return empty string for contest round label', () => { |
| 204 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 205 | + |
| 206 | + expect(provider.getContestRoundLabel('practice2')).toBe(''); |
| 207 | +}); |
| 208 | +``` |
| 209 | + |
| 210 | +#### テスト1.6: テストケースデータ検証 |
| 211 | + |
| 212 | +準備されたテストケースデータが正しく構成されていることを確認。 |
| 213 | + |
| 214 | +```typescript |
| 215 | +test('expects test data to have 12 tasks with correct properties', () => { |
| 216 | + expect(taskResultsForACLPracticeProvider).toHaveLength(12); |
| 217 | + expect(taskResultsForACLPracticeProvider.every((task) => task.contest_id === 'practice2')).toBe( |
| 218 | + true, |
| 219 | + ); |
| 220 | + |
| 221 | + const expectedIndices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']; |
| 222 | + const actualIndices = taskResultsForACLPracticeProvider.map((task) => task.task_table_index); |
| 223 | + |
| 224 | + expect(actualIndices).toEqual(expectedIndices); |
| 225 | +}); |
| 226 | +``` |
| 227 | + |
| 228 | +#### テスト1.7: フィルタリング統合テスト |
| 229 | + |
| 230 | +実際のテストケースデータを使用して、フィルタリング機能を検証。 |
| 231 | + |
| 232 | +```typescript |
| 233 | +test('expects to filter test data correctly', () => { |
| 234 | + const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); |
| 235 | + const allTasks = [...taskResultsForACLPracticeProvider, ...someOtherContestTasks]; |
| 236 | + |
| 237 | + const filtered = provider.filter(allTasks); |
| 238 | + |
| 239 | + expect(filtered).toHaveLength(12); |
| 240 | + expect(filtered).toEqual(taskResultsForACLPracticeProvider); |
| 241 | +}); |
| 242 | +``` |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## 実装ステップ |
| 247 | + |
| 248 | +### ステップ 1: テストケースデータの追加 |
| 249 | + |
| 250 | +`src/test/lib/utils/test_cases/contest_table_provider.ts` に以下を追加: |
| 251 | + |
| 252 | +1. `taskResultsForACLPracticeProvider` 定数の定義 |
| 253 | +2. `createContestTasksForACLPractice` ヘルパー関数の定義 |
| 254 | + |
| 255 | +### ステップ 2: テストケースのエクスポート |
| 256 | + |
| 257 | +`src/test/lib/utils/test_cases/contest_table_provider.ts` のエクスポート一覧に `taskResultsForACLPracticeProvider` を追加 |
| 258 | + |
| 259 | +### ステップ 3: テストスイートの追加 |
| 260 | + |
| 261 | +`src/test/lib/utils/contest_table_provider.test.ts` に以下を追加: |
| 262 | + |
| 263 | +1. インポート文に `taskResultsForACLPracticeProvider` を追加 |
| 264 | +2. 「JOI First Qual Round provider」セクションの直前に「ACL Practice Provider」セクションを追加 |
| 265 | +3. 上記のテストケース(1.1~1.7)を実装 |
| 266 | + |
| 267 | +### ステップ 4: テスト実行と検証 |
| 268 | + |
| 269 | +```bash |
| 270 | +pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts |
| 271 | +``` |
| 272 | + |
| 273 | +すべてのテストが PASSすることを確認 |
| 274 | + |
| 275 | +--- |
| 276 | + |
| 277 | +## 注記 |
| 278 | + |
| 279 | +- テストケースのステータス分布は現実的な使用パターンを反映:初期問題は解けているが、高度な問題は挑戦中 |
| 280 | +- ACLPracticeProvider は EDPCProvider と同じ表示設定を共有するため、比較的シンプルなテスト設計が可能 |
| 281 | +- テストは JOI First Qual Round provider の前に配置されることで、テストスイートの論理的な順序を保つ |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +## 実装後の教訓 |
| 286 | + |
| 287 | +### ✅ 実装完了 |
| 288 | + |
| 289 | +- **テスト結果**: 203/203 PASS ✅ |
| 290 | +- テストケースデータ追加 + テストスイート実装完了 |
| 291 | + |
| 292 | +### 📌 重要な学習ポイント |
| 293 | + |
| 294 | +#### 1. **モック関数の漏れは必ず発生する** ⚠️ |
| 295 | + |
| 296 | +- `classifyContest` モックに `practice2` → `ContestType.ACL_PRACTICE` を忘れずに追加すること |
| 297 | +- **毎回チェックリスト**: |
| 298 | + - [ ] 新しい contest_id に対応するモック処理を追加したか |
| 299 | + - [ ] テストデータの contest_id とモックの対応が一致しているか |
| 300 | + - [ ] 初回実行で失敗した場合、モック定義を最優先で確認すること |
| 301 | + |
| 302 | +#### 2. **既存ヘルパー関数の活用** |
| 303 | + |
| 304 | +- `createContestTasks` 関数を使用することで、テストデータの一貫性を維持 |
| 305 | +- 手動で TaskResult を構築するより、ヘルパー関数を優先する |
| 306 | + |
| 307 | +#### 3. **統合テストの重要性** |
| 308 | + |
| 309 | +- 単体テスト(個別コンテスト)だけでなく、同一問題で複数コンテストが混在するテストも必須 |
| 310 | +- フィルタリングの正確性確保には、他のコンテストデータとの組み合わせが効果的 |
| 311 | + |
| 312 | +#### 4. **テスト配置の順序** |
| 313 | + |
| 314 | +- 新しいテストスイートは既存のセクション構成を考慮して配置 |
| 315 | +- 論理的な順序(年代順・難易度順など)を保つことで保守性が向上 |
0 commit comments