phasegate 0.134.0 → 0.136.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.ja.md +4 -4
  3. package/README.md +4 -3
  4. package/bin/phasegate +2 -0
  5. package/docs/contracts/lesson-artifact.schema.json +62 -0
  6. package/docs/contracts/requirement-test-matrix.schema.json +62 -0
  7. package/docs/guide/layer-model.md +2 -2
  8. package/docs/principles/architecture-philosophy.md +26 -55
  9. package/docs/principles/model-routing.md +30 -165
  10. package/docs/principles/testing-rules.md +66 -648
  11. package/docs/templates/ci/aidlc-gate.yml +97 -0
  12. package/docs/templates/ci/consistency-check.yml +117 -0
  13. package/docs/templates/hooks/commit-msg +13 -0
  14. package/docs/templates/hooks/pre-commit +62 -0
  15. package/package.json +5 -3
  16. package/scripts/harness/ci-governance/composition-root.ts +1 -1
  17. package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts +14 -31
  18. package/scripts/harness/ci-governance/presentation/handlers/generate-ci-template-handler.ts +19 -1
  19. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
  20. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  21. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  22. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  23. package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
  24. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
  25. package/scripts/harness/main.ts +19 -7
  26. package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
  27. package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
  28. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
  29. package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
  30. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
  31. package/scripts/harness/setup/skill-deployer.ts +48 -2
  32. package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
  33. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +3 -1
  34. package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
  35. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
  36. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
  37. package/scripts/harness/validator-system/composition-root.ts +8 -2
  38. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -4
  39. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
  40. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +11 -2
  41. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +4 -0
@@ -1,670 +1,88 @@
1
1
  # テスト規約
2
2
 
3
- ```
4
- 凡例
5
- #テストレイヤー
6
- ##規約タイトル
7
- ###OK
8
- ###NG
9
- ```
10
-
11
- ---
12
-
13
- ![image.png](%E3%83%86%E3%82%B9%E3%83%88%E8%A6%8F%E7%B4%84/image.png)
14
-
15
- # Index
16
-
17
- ### 全テスト共通
18
-
19
- [テスト関連ファイルの名称はすべてkebab-caseにする](https://www.notion.so/kebab-case-1d65fb52a4aa80d99cc6fb336a528a64?pvs=21)
20
-
21
- [テストケース名は全て日本語で記述する](https://www.notion.so/1d65fb52a4aa80e8a340e04abf238806?pvs=21)
22
-
23
- [実装の詳細はテストケース名に表さない](https://www.notion.so/1dd5fb52a4aa80fcbb5cc5b99d79fc80?pvs=21)
24
-
25
- [自動テストは**AAAパターンで記述する**](https://www.notion.so/AAA-1e55fb52a4aa80e7b917e460c8fef6c9?pvs=21)
26
-
27
- ### テストサイズ:Large
28
-
29
- **E2Eテスト**
30
-
31
- [E2Eテストは、ドメインエキスパートやPdMやCSが見てわかる内容にする](https://www.notion.so/E2E-PdM-CS-1dc5fb52a4aa80bf8e6cc88ccd2ea5f1?pvs=21)
32
-
33
- ### テストサイズ:Medium
34
-
35
- **Page UIテスト**
36
-
37
- [バックエンドとの通信はMSWを使ってMockする](https://www.notion.so/MSW-Mock-22b5fb52a4aa80848a7ec39502a7e25a?pvs=21)
38
-
39
- **インテグレーションテスト**
40
-
41
- [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1d65fb52a4aa806a8690d2938eab0e0c?pvs=21)
42
-
43
- [テストケースを記述するdescribe()とit()の書きっぷり](https://www.notion.so/describe-it-1e35fb52a4aa8045a359f847fcc4f5ee?pvs=21)
44
-
45
- ### テストサイズ:Small
46
-
47
- **ユニットテスト**
48
-
49
- [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1dc5fb52a4aa80cf92f0ec74c5b5ac18?pvs=21)
50
-
51
- [**(WIP)**モックオブジェクトは外部依存に対してのみ利用する](https://www.notion.so/WIP-1e45fb52a4aa8004ac43daca7af73be7?pvs=21)
52
-
53
- [テストケースを記述するdescribe()とit()の書きっぷり](https://www.notion.so/describe-it-1e65fb52a4aa80b7ac04f003a84468eb?pvs=21)
54
-
55
- ---
56
-
57
- # 全テスト共通
58
-
59
- ## テスト関連ファイルの名称はすべてkebab-caseにする
60
-
61
- ### OK
62
-
63
- ```
64
-
65
- fetch-contract-v2-by-user.seed.ts
66
- ```
67
-
68
- ### NG
69
-
70
- ```
71
- fetchContractV2ByUser.seed.ts
72
- FetchContractV2ByUser.seed.ts
73
- fetch_contract_V2_by_user.seed.ts
74
- ```
75
-
76
- ### 背景
77
-
78
- Gitはファイル名の大文字・小文字を区別しない設定になっていることがあり、ファイル名をたとえば `FileName.txt` から `filename.txt` に変更しても、Gitが変更を検出せずに無視してしまい予期せぬインポートエラーなどが発生するリスクがあるため
79
-
80
- ### 捕捉
81
-
82
- テスト関連ファイルに限らず、基本的にすべてkebabケースでファイル名称を統一する。
83
-
84
- (例外として、hostingのコンポーネントに関してのみcamelで現状書き進めているので保留)
85
-
86
- ## テストケース名は全て日本語で記述する
87
-
88
- ### OK
89
-
90
- ```tsx
91
- //e2e
92
- ## 拠点内のフリースペースの情報が一覧で表示される
93
- * フリースペースマスタ一覧の"1"行目の"定員"に"10"が表示されている
94
-
95
- //インテグレーションテスト,ユニットテスト
96
- it('データに不備がある契約明細をインポートした際に、インポートに失敗すること', async () => {}
97
- ```
98
-
99
- ### NG
100
-
101
- ```tsx
102
- //e2e
103
- ## Free space information can be displayed in list
104
- * The "Capacity" column in the first row of the Free Space Master list displays "10".
105
-
106
- // インテグレーションテスト,ユニットテスト
107
- it('Fails - returns 404 when resource does not exis', async()=>{})
108
- ```
109
-
110
- ### 背景
111
-
112
- - 動く仕様書として実行可能なテストを書く戦略
113
-
114
- ### 捕捉
115
-
116
- - テストケースの書きっぷりについては以下を参照。
117
- - [E2Eテストは、ドメインエキスパートやPdMやCSが見てわかる内容にする](https://www.notion.so/E2E-PdM-CS-1dc5fb52a4aa80bf8e6cc88ccd2ea5f1?pvs=21)
118
- - [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1d65fb52a4aa806a8690d2938eab0e0c?pvs=21)
119
- - [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1dc5fb52a4aa80cf92f0ec74c5b5ac18?pvs=21)
120
-
121
- ## 実装の詳細はテストケース名に表さない
122
-
123
- ### OK
124
-
125
- ```tsx
126
- it('渡されたプロバイダーIDの中にLINEがある場合はLINEの問い合わせ先を返す')
127
- ```
128
-
129
- ### NG
130
-
131
- ```tsx
132
- it('externalUserInfoIdに紐づくデータがUserDBに存在する場合、UserInfoModelを返す(user = test1@exapmle.com)', async () => {}
133
- // 内部のプロパティ名やクラス名などの実装に、テストケースの記述が依存している
134
- // 仕様とは関係ない、テストデータの情報が書かれている
135
- ```
136
-
137
- ### 背景
138
-
139
- - 実装の詳細に依存したテストの場合、**リファクタリングへの耐性が失われ、壊れやすいテストになってしまう。**その結果以下のような弊害が生じる。
140
- - リファクタリングのハードルが上がってしまう。例えばexternalUserInfoIdというプロパティがリファクタリングされてプロパティ名が変更された場合、テストケース名も修正する必要が出てくる
141
- - 実装の詳細まで知らないと(見に行かないと)そのテストケースで担保したいことが理解できない(読みづらい仕様書になる=テストコードの理解容易性が損なわれる)
142
- - [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1d65fb52a4aa806a8690d2938eab0e0c?pvs=21)(インテグレーションテスト)
143
- - [テストケース名は、**何も知らない**開発者が見てわかる内容にする](https://www.notion.so/1dc5fb52a4aa80cf92f0ec74c5b5ac18?pvs=21) (ユニットテスト)
144
-
145
- ## 自動テストは**AAAパターンで記述する**
146
-
147
- ### OK
148
-
149
- ```tsx
150
- it('ユーザーが存在する場合、UserWishRepositoryから取得した結果をそのまま返すこと', async () => {
151
- // Arrange
152
- defaultMocks(...)//テストの関心ごとでない共通Arrangeを呼び出し
153
- mockUserRepository.isExisting = vi.fn().mockResolvedValue(true);
154
- const userWishModels = [new InProgressUserWishModel({ userWishId: 'wish-1', content: 'content-1' })];
155
- mockUserWishRepository.findMultiByUserId = vi.fn().mockResolvedValue(userWishModels);
156
- const userId = "test_user";
157
- const baseId = "test_base";
158
-
159
- // Act
160
- const result = await sut.findUserWishByUserIdAndBaseId(userId, baseId, [UserWishStatus.IN_PROGRESS]);
161
-
162
- // Assert
163
- expect(mockUserRepository.isExisting).toHaveBeenCalledWith(userId);
164
- expect(result).toEqual(userWishModels);
165
- });
166
- });
167
- ```
168
-
169
- ### NG
170
-
171
- ```tsx
172
- describe('ユーザーWishの情報を取得する', () => {
173
- beforeEach(() => {
174
- vi.clearAllMocks();
175
- vi.resetAllMocks();
176
- usecase = reserveSpaceUsecase(dependencies);
177
- //必要なArrangeがテストの外で行われている
178
- mockIssueNoService.issueNo = vi.fn().mockResolvedValue('RES001');
179
- mockRefundSpaceReservationService.calcReservationAmount = vi.fn().mockReturnValue(1100);
180
- mockGoogleCalendarService.authenticate = vi.fn().mockResolvedValue(undefined);
181
- });
182
-
183
- it('ユーザーが存在する場合、UserWishRepositoryから取得した結果をそのまま返すこと', async () => {
184
- // テストの関心ごとでないmockも直接Arrangeしている
185
- mockSpaceReservationRepository.getDupricatedReservationsCount = vi.fn().mockResolvedValue(0);
186
- mockSpaceReservationRepository.findMultiByGoogleCalendarEventId = vi.fn().mockResolvedValue([]);
187
- mockSpaceReservationRepository.save = vi.fn().mockResolvedValue(undefined);
188
- mockUserRepository.isExisting = vi.fn().mockResolvedValue(true);
189
- const userWishModels = [new InProgressUserWishModel({ userWishId: 'wish-1', content: 'content-1' })];
190
- mockUserWishRepository.findMultiByUserId = vi.fn().mockResolvedValue(userWishModels);
191
- const userId = "test_user";
192
- const baseId = "test_base";
193
-
194
- // Act
195
- const result = await sut.findUserWishByUserIdAndBaseId(userId, baseId, [UserWishStatus.IN_PROGRESS]);
196
-
197
- // Assert
198
- expect(mockUserRepository.isExisting).toHaveBeenCalledWith(userId);
199
- expect(result).toEqual(userWishModels);
200
- });
201
- });
202
- });
203
- ```
204
-
205
- ### 背景
206
-
207
- - AAAパターンとはテストケース構造に関するパターンのこと。このパターンでは、テストケースを準備(Arrange)、実行(Act)、確認(Assert)の3つのフェーズで構成される。
208
- - AAAパターンを用いることで、テストスイートに含まれるすべてのテストケースに対して簡潔で統一された構造を持たせられるようになる
209
- - この構造を維持すると、どのようなテストケースであっても可読性が向上する
210
- - そのテストケースに必要な処理が凝集するため**テストの独立性**が保たれる(これがデカい)
211
- - 結果的にテストの保守コストを大きく削減できる
212
-
213
- ### 補足
214
-
215
- - テストの関心ごとでない、共通部分のArrangeを複数のテストケースで行いたい場合に、そのまま記述すると冗長な書き方になるため、オブジェクトマザーやファクトリーなどで工夫する必要がある
216
-
217
- ### 関連
218
-
219
- - Assert First(Assertionから書くTDDプロセスの基本テクニック)
220
-
221
- [[enknot LT] AAAの話](https://www.notion.so/enknot-LT-AAA-22b5fb52a4aa8078a0abe1ba35af86f8?pvs=21)
222
-
223
- ## テストの実行結果はactualに代入する
224
-
225
- ### OK
226
-
227
- ```tsx
228
- const actual = await usecase.execute(spaceId, baseId);
229
-
230
- expect(actual).toBeUndefined();
231
- ```
232
-
233
- ### NG
234
-
235
- ```tsx
236
- const result = await usecase.execute(spaceId, baseId);
237
-
238
- expect(result).toBeUndefined();
239
- ```
240
-
241
- 変数名を統一するため、resultには代入しないようにする
242
-
243
- ---
244
-
245
- # E2Eテスト / シードデータ
246
-
247
- ## テストケース固有のデータは専用のseedファイルで管理する
248
-
249
- ### OK
250
-
251
- ```
252
- e2e/seeds/
253
- ├── users.seed.ts # 全テスト共通のユーザーデータ
254
- └── dev-dummy-process.seed.ts # DevDummyプロセス固有のテストデータ
255
- ```
256
-
257
- ```typescript
258
- // dev-dummy-process.seed.ts
259
- export async function seedDevDummyProcesses(): Promise<void> {
260
- // テストケースに必要なプロセスデータを作成
261
- }
262
- ```
263
-
264
- ### NG
265
-
266
- ```typescript
267
- // spec.tsの中でbeforeEachにデータセットアップを直書きする
268
- beforeEach(async () => {
269
- // 他のテストケースでも使い回せるデータを直書きしている
270
- await createProcess({ label: 'テスト用' });
271
- });
272
- ```
273
-
274
- ### 背景
275
-
276
- - テストデータの独立性を担保するため、テストケース固有のデータセットアップは専用のseedファイルに切り出す
277
- - 共通データ(ユーザー等)と機能固有データを分離することで、テストスイートの見通しが良くなる
278
- - seedファイルを分けることで、特定の機能のテストデータだけを再作成・クリーンアップしやすくなる
279
-
280
- ### 補足
281
-
282
- - 全テスト共通のデータ(ユーザー等)は `users.seed.ts` のような共通seedに置く
283
- - 特定のテストケースにしか使わないデータは、そのテストケース内で `afterEach` によるクリーンアップとセットで管理する
284
-
285
- ---
286
-
287
- # E2Eテスト
288
-
289
- ## E2Eテストは実行リソースを考慮してテストケースを統合する
290
-
291
- ### 方針
292
-
293
- E2Eテストは1ケースごとにログイン・データ作成・API呼び出し等のセットアップコストが高い。そのため、以下の条件を満たすテストケースは1つのテストケースに統合する。
294
-
295
- 1. **同一のArrangeを共有している**: プロセス作成やイベント発火など、事前準備が同じテストケース群
296
- 2. **段階的に検証できる**: 1つのテスト内でAct→Assertを繰り返すことで、ライフサイクル全体を一貫して検証できる場合
297
- 3. **前のステップの結果が次の前提条件になっている**: テストケース間に因果関係がある場合
298
-
299
- ### OK
300
-
301
- ```typescript
302
- // 1つのテストで確認依頼のライフサイクル全体を検証
303
- test('確認依頼のライフサイクル: 生成→対応済み→再生成', async ({ page }) => {
304
- // Arrange: プロセス作成(1回だけ)
305
- const { devDummyProcessId } = await createDevDummyProcess(page, { ... });
306
-
307
- // Act & Assert 1: AI_EXECUTED → PENDINGで表示される
308
- await fireEvent(page, devDummyProcessId, { eventType: 'AI_EXECUTED', ... });
309
- await expect(page.getByTestId('intervention-request-status')).toHaveText('PENDING');
310
-
311
- // Act & Assert 2: INTERVENTION → 未対応一覧から消える
312
- await fireEvent(page, devDummyProcessId, { eventType: 'INTERVENTION', ... });
313
- // ... RESPONDEDに遷移したことを確認
314
-
315
- // Act & Assert 3: 再度AI_EXECUTED → 新しい未対応が1件
316
- await fireEvent(page, devDummyProcessId, { eventType: 'AI_EXECUTED', ... });
317
- // ... 未対応が1件のみであることを確認
318
- });
319
- ```
320
-
321
- ### NG
322
-
323
- ```typescript
324
- // 同一Arrangeのテストケースを個別に分けている(リソースの無駄)
325
- test('確認依頼がPENDINGで表示される', async ({ page }) => {
326
- const { devDummyProcessId } = await createDevDummyProcess(page, { ... });
327
- await fireEvent(page, devDummyProcessId, { eventType: 'AI_EXECUTED', ... });
328
- // Assert...
329
- });
330
-
331
- test('介入後に確認依頼がRESPONDEDになる', async ({ page }) => {
332
- const { devDummyProcessId } = await createDevDummyProcess(page, { ... }); // 同じArrangeを繰り返し
333
- await fireEvent(page, devDummyProcessId, { eventType: 'AI_EXECUTED', ... });
334
- await fireEvent(page, devDummyProcessId, { eventType: 'INTERVENTION', ... });
335
- // Assert...
336
- });
337
- ```
338
-
339
- ### 背景
340
-
341
- - E2Eテストはテストピラミッドの頂点に位置し、実行コストが最も高い
342
- - 1ケースごとにブラウザ操作・認証・データ作成・API呼び出しが発生するため、不必要にケースを分割するとCI/CDパイプラインの実行時間が増大する
343
- - 同一のArrangeを共有するテストケースを統合することで、セットアップコストを1回に削減しつつ、ライフサイクル全体の整合性も検証できる
344
-
345
- ### 補足
346
-
347
- - 統合の判断基準は「Arrangeの共有度」と「テストケース間の因果関係」
348
- - 独立した前提条件を持つテストケース(例: 異なるユーザーロール、異なるプロセス状態)は統合しない
349
- - 表示系の検証(タイトル・クライアント名・バッジ等)も同一Arrangeであれば1テスト内で段階的にAssertする
350
-
351
- ---
352
-
353
- ## E2Eテストは、ドメインエキスパートやPdMやCSが見てわかる内容にする
354
-
355
- ### OK
356
-
357
- ```tsx
358
- //edit.spec
359
- * フリースペースID"1"に紐づくフリースペースマスタ編集画面を直接開く
360
- * 編集フォームに定員"10"を入力する
361
- * 保存ボタンを押下する(画面が自動更新される)
362
- * 定員が"10"で表示されていることを確認する
363
- ```
364
-
365
- ### NG
366
-
367
- ```tsx
368
- //edit.spec
369
- * freeSpaceId"1"に紐づくFreeSpaceMasterEditScreenを直リンでwindow.openする
370
- * FreeSpaceMasterEditScreenのcapacityフォームに"10"をインプットする
371
- * 保存ボタンを押す
372
- * 指定したfreeSpaceIdに紐づく単一FreeSpaceのcapacityの値が"10"である
373
- ```
374
-
375
- ### 背景
376
-
377
- - まず自然言語で仕様を言語化し、その言語化された内容にそって自動テストを書き、実装することによって**「動く仕様書」**を整え、運用する方針
378
- - そのため、テストケースには「仕様書としての表現力」が備わっていることが重要
379
- - E2Eテストは、ユーザーストーリーの受け入れ基準となるテストケースであり、ユーザーストーリーが提供する機能的なふるまいの価値を表現するものになるため、非開発メンバーが見てもわかる表現であることが重要
380
-
381
- ---
382
-
383
- # Page UIテスト
384
-
385
- ### 背景
386
-
387
- - まず自然言語で仕様を言語化し、その言語化された内容にそって自動テストを書き、実装することによって**「動く仕様書」**を整え、運用する方針
388
- - そのため、テストケースには「仕様書としての表現力」が備わっていることが重要
389
- - PageUIのテストは、画面要素の操作とその結果の挙動をテストケースとして表現するものであり、エンジニアがその画面の仕様と動作を正確に認知できる表現であることが重要
390
-
391
- ## バックエンドとの通信はMSWを使ってMockする
392
-
393
- ### OK
394
-
395
- ```tsx
396
- // xxxx.handler.ts
397
- export const defaultMockHandlers = () => {
398
- // NOTE: Cloud Functions
399
- const baseMockHandler = getFetchBaseMockHandler(base);
400
- const contractMockHandler = getFetchContractV2ByUserMockHandler();
401
- const spaceMockHandler = getSearchSpacesMockHandler({ spaces: [] });
402
- const fetchSpaceMockHandler = getFetchSpaceUsagesMockHandler();
403
- const fetchUser = getFetchUserByExternalIdMockHandler(user);
404
- // NOTE: Cloud Run
405
- const roleMockHandler = getCreateUserRolePermissionMockHandler();
406
-
407
- return [baseMockHandler, contractMockHandler, spaceMockHandler, roleMockHandler, fetchSpaceMockHandler, fetchUser];
408
- };
409
-
410
- // xxxx.spec.ts
411
- target('リソースカテゴリー(予約画面の最上部にあるタグ)', () => {
412
- describe('画面を開いたユーザーの属性に応じて、表示されるカテゴリーが変化する', () => {
413
- context('一般利用者の場合', () => {
414
- it('一般利用者が予約可能なカテゴリーのみが表示されていること', async ({ network, page }) => {
415
- const defaultMock = defaultMockHandlers();
416
- // arrange
417
- network.use(...defaultMock);
418
- // act
419
- await page.goto('http://localhost:3000/base/test-base/space/reserve');
420
- // assert
421
- await expect(page.getByRole('tab', { name: '会議室1' })).toBeVisible();
422
- await expect(page.getByRole('tab', { name: '会議室2' })).not.toBeVisible();
3
+ この文書をテスト規約の正本とする。外部ドキュメントへの依存は置かない。
4
+
5
+ ## 共通ルール
6
+
7
+ - テスト関連ファイル名は kebab-case にする。
8
+ - テストケース名は日本語で、仕様として読める表現にする。
9
+ - テストケース名に実装詳細、内部クラス名、変数名、テストデータの都合を混ぜない。
10
+ - テストは Arrange / Act / Assert を明確に分ける。
11
+ - Act は原則 1 つにする。段階的なライフサイクルを検証する E2E では、各ステップの Act / Assert が読める形にする。
12
+ - 実行結果は `actual` に代入する。
13
+ - テストの関心外の共通準備はヘルパーやファクトリに寄せる。ただし、各テストの前提が読めなくなるほど隠蔽しない。
14
+
15
+ ## テスト構造
16
+
17
+ `target` / `describe` / `context` / `it` は以下の役割で使う。
18
+
19
+ | 要素 | 役割 |
20
+ |------|------|
21
+ | `target` | テスト対象の機能・関数・ユースケース |
22
+ | `describe` | 検証するふるまい |
23
+ | `context` | ふるまいが変わる前提条件 |
24
+ | `it` | 期待結果 |
25
+
26
+ ```ts
27
+ target('validateConfig', () => {
28
+ describe('設定ファイルを検証する', () => {
29
+ context('必須項目が不足している場合', () => {
30
+ it('検証エラーを返すこと', () => {
31
+ // Arrange
32
+ const input = {};
33
+
34
+ // Act
35
+ const actual = validateConfig(input);
36
+
37
+ // Assert
38
+ expect(actual.valid).toBe(false);
423
39
  });
424
40
  });
425
41
  });
426
42
  });
427
43
  ```
428
44
 
429
- ### NG
430
-
431
- ```tsx
432
- // mockせずに正規のバックエンドとデータベースを使用する
433
- ```
45
+ ## モック方針
434
46
 
435
- ### 背景
47
+ - ドメインオブジェクトや値オブジェクトは原則として実体を使う。
48
+ - モックは外部 I/O、時刻、乱数、プロセス外サービス、Port 実装など、テスト対象が直接制御できない依存に限定する。
49
+ - UseCase の単体テストでは Port はモックしてよいが、Domain のふるまいは実体で検証する。
50
+ - Page UI テストでバックエンド通信を置き換える場合は MSW などのリクエスト境界でモックする。
436
51
 
437
- - 正規のバックエンドとデータベースを使用するとテストデータの作成コストもテストの実行コストも大きいが、apiをmockすることでコストを削減できる
438
- - テストには直接関わらないデータベースの依存関係などを気にしなくていい
439
- - http通信やバックエンドの計算がなくなることで実行時間が短縮
440
- - その他にも、テストの安定性向上やフロント単体での実装が可能になるメリットがある
441
- - レスポンスが固定されることでFlakyテストの発生を抑制
442
- - バックエンド未実装でもフロント単体で実装可能
52
+ ## E2E テスト
443
53
 
444
- ### 補足
54
+ - E2E はユーザー価値や受け入れ基準として読める名前・手順にする。
55
+ - ドメインエキスパート、PdM、CS が読んでも意図が分かる表現にする。
56
+ - 同一 Arrange を共有し、前の結果が次の前提になる一連のライフサイクルは 1 つのテストに統合してよい。
57
+ - 独立した前提条件を持つケースは分ける。
58
+ - テスト固有データは専用 seed やファクトリで管理し、共通データと機能固有データを混ぜない。
445
59
 
446
- - Cloud Functionのmock handlerは`hosting/test/page-ui/utils/msw/cloud-function.msw.ts`に定義済み
447
- - Cloud Runのmock handlerはOrvalで`hosting/src/__generated`配下に自動生成される
448
- - 存在しない場合は`npm run generate`
449
- - mockする際、responseを任意の値にしたい場合は値をhandlerに渡し、どんな値でも良い場合は何も渡さずに実行すればよい
450
- - firebase Authenticationは初期状態でダミーのユーザーとして認証される
451
- - httpリクエストが発生したURLに対して複数のhandlerがヒットした場合、先に定義されたものが優先される。共通handlerと個別handlerを定義した場合は個別handlerを先に使用する
452
- - `network.use(...overrideMock, ...defaultMock);`
60
+ ## Integration テスト
453
61
 
454
- ---
62
+ - 外部から観察できる I/O とふるまいを検証する。
63
+ - ケース名は、対象実装を知らない開発者が読んでも入力条件と期待結果が分かる表現にする。
64
+ - 独自接頭辞、内部パラメータ名、テストデータ名をケース名に入れない。
455
65
 
456
- # インテグレーションテスト
66
+ ## Unit テスト
457
67
 
458
- ## テストケース名は、**何も知らない**開発者が見てわかる内容にする
459
-
460
- ### OK
461
-
462
- ```tsx
463
- it('指定したスペースIDが存在しない場合は、404を返す', async () => {}
464
- ```
68
+ - 小さなドメインルール、値オブジェクト、サービス、UseCase の分岐を焦点化して検証する。
69
+ - 1 テスト 1 ルールを基本とし、境界値や異常系は分ける。
70
+ - リファクタリングで壊れないよう、内部実装ではなく外部から見えるふるまいを検証する。
465
71
 
466
- ### NG
72
+ ## 禁止例
467
73
 
468
- ```tsx
469
- it('FAILURE1 - 指定したパラメータのidが不正なパターン(user = test1@exapmle.com)', async () => {}
470
- // 統一されていない独自ルールの接頭辞
471
- // 指定したパラメータとは何か、テストメソッドの内部をしっかり確認しに行かないとI/Oが把握できない(テスト実装者のみ、指定したパラメータとは何か頭の中で分かっている)
472
- // 不正とはどういう状態なのかわからない
473
- // テストケースとは直接関係ない実装の知識やテストデータの詳細が滲んでいる
474
- ```
475
-
476
- ### 背景
477
-
478
- - まず自然言語で仕様を言語化し、その言語化された内容にそって自動テストを書き、実装することによって**「動く仕様書」**を整え、運用する方針
479
- - そのため、テストケースには「仕様書としての表現力」が備わっていることが重要
480
-
481
- ### 捕捉
482
-
483
- - 「何も知らない開発者」とは、例えば以下のような人物を指す。
484
- - 他のチームや新しくチームに入った開発者
485
- - 記憶をなくした未来の自分
486
- - [実装の詳細はテストケース名に表さない](https://www.notion.so/1dd5fb52a4aa80fcbb5cc5b99d79fc80?pvs=21)
487
-
488
- ## テストケースを記述するdescribe()とit()の書きっぷり
489
-
490
- ### OK
491
-
492
- ```tsx
493
- import { describe, expect, it } from 'vitest';
494
- import { context, target } from '../../helper/common-helper';
495
-
496
- target('saveQuestionnaireAnswer', () => {
497
- describe('アンケート回答情報を受け取り、保存した回答IDを返す', () => {
498
- it('アンケート回答情報を受け取り、ステータスコード200と保存した回答IDを返す', () => {
499
- // テストの実装を書く
500
- });
501
- context('対象のquestionnaireが存在しない時', () => {
502
- it('ステータスコード404を返す', () => {
503
- // テストの実装を書く
504
- });
505
- });
506
- });
507
- describe('アンケート回答の保存が成功した場合、回答者にメッセージを配信する', () => {
508
- it('LINEメッセージを配信する', () => {
509
- // テストの実装を書く
510
- });
511
- it('メールを配信する', () => {
512
- // テストの実装を書く
513
- });
514
- });
74
+ ```ts
75
+ // NG: 英語、実装詳細、独自接頭辞、result 変数
76
+ it('FAILURE1 - externalUserInfoId is invalid', async () => {
77
+ const result = await sut.execute(input);
78
+ expect(result).toEqual(error);
515
79
  });
516
-
517
- ```
518
-
519
- ### NG
520
-
521
- ```tsx
522
- import { describe, expect, it } from 'vitest';
523
- import { context, target } from '../../helper/common-helper';
524
-
525
- // テスト対象のクラス名を記載してしまっている
526
- // describeのエイリアスを使えていない
527
- describe('ContractInventionRecipientModel', ()=>{
528
- describe('saveQuestionnaireAnswer', () => {
529
- describe('アンケート回答情報を受け取り、保存した回答IDを返す', () => {
530
- it('アンケート回答情報を受け取り、ステータスコード200と保存した回答IDを返す', () => {
531
- // テストの実装を書く
532
- });
533
- });
534
- });
535
- })
536
80
  ```
537
81
 
538
- ### 構文解説
539
-
540
- - target..テスト対象のメソッド
541
- - describe…テスト対象メソッドのふるまい説明
542
- - (context…検証したいふるまいに前提条件があればcontextとして記載)
543
- - it…期待値
544
-
545
- ### 背景
546
-
547
- - テストコードは上から下に読みやすい形式で書きたいため
548
- - itに条件まで記載すると認知負荷が高まってしまう
549
- - テストコードをtargetやcontextの観点で構造化することで、各テストケースの目的や前提条件がより明確となり、全体の可読性と保守性が向上する
550
-
551
- ---
552
-
553
- # ユニットテスト
554
-
555
- ## テストケース名は、**何も知らない**開発者が見てわかる内容にする
556
-
557
- ### OK
558
-
559
- ```tsx
560
- it('渡されたプロバイダーIDの中にLINEがある場合はLINEの問い合わせ先を返す', () => {}
561
- ```
562
-
563
- ### NG
564
-
565
- ```tsx
566
- it('ProviderIdに基づきLineContactPointModelを返す', () => {}
567
- // 内部のプロパティ名やクラス名にテストケースの記述が依存しているため、内部実装を把握していないと振る舞いがわかりにくい
568
- ```
569
-
570
- ### 背景
571
-
572
- - まず自然言語で仕様を言語化し、その言語化された内容にそって自動テストを書き、実装することによって**「動く仕様書」**を整え、運用する方針
573
- - そのため、テストケースには「仕様書としての表現力」が備わっていることが重要
574
-
575
- ### 捕捉
576
-
577
- - 「何も知らない開発者」とは、例えば以下のような人物を指す。
578
- - 他のチームや新しくチームに入った開発者
579
- - 記憶をなくした未来の自分
580
- - [実装の詳細はテストケース名に表さない](https://www.notion.so/1dd5fb52a4aa80fcbb5cc5b99d79fc80?pvs=21)
581
-
582
- ### **(WIP)**モックオブジェクトは外部依存に対してのみ利用する
583
-
584
- ### OK
585
-
586
- ```tsx
587
-
588
- ```
589
-
590
- ### NG
591
-
592
- ```tsx
593
-
594
- ```
595
-
596
- ### 背景
597
-
598
- - 管理下にある外部依存はモックしない
599
- - テスト対象のアプリケーションが自由に扱えるもの
600
- - アクセス対象のプロセス外依存にアクセスするのにテスト対象だけが経由できる状況は、実質的には実装の詳細と言える(外部から観察できない)
601
- - これをモックにしてしまうと、偽陽性が高まる(モックを使っているのでテストは通るが、実際には通らないプログラムが生まれやすくなる)
602
- - 管理下にない外部依存のみ、モックを利用する
603
- - テスト対象のアプリケーションが自由に扱えないもの
604
- - 外部から観察できる
605
- - テスト対象と管理下にない外部依存との間のコミュニケーションが正しく行われているかを担保するために、モックを使ってテストする
606
- - Usecaseレイヤー単体から見ると、Portは管理下にない外部依存であり、Domainは管理下にある外部依存である
607
- - 前者はモックを利用し、後者はMockを利用せずに実体を使ってテストする
608
- - データ取得・保存の「結果」ではなく「ビジネスルールの適用プロセス」を検証するというユースケースの単体テストの目的にも合致する(そのための依存性逆転)
609
-
610
- ## テストケースを記述するdescribe()とit()の書きっぷり
611
-
612
- ### OK
613
-
614
- ```jsx
615
- import { describe, expect, it } from 'vitest';
616
- import { context, target } from '../../helper/common-helper';
617
-
618
- target('getBaseContactPoint', () => {
619
- describe('providerIdsに応じた連絡先を返す', () => {
620
- context('providerIdsにLINEが含まれている場合', () => {
621
- it('LINEの問い合わせ先を返す', () => {
622
- // テストの実装を書く
623
- });
624
- });
625
- context('providerIdsにLINEが含まれていない場合', () => {
626
- it('EMAILの問い合わせ先を返す', () => {
627
- // テストの実装を書く
628
- });
629
- });
630
- context('providerIdsの配列が空の場合', () => {
631
- it('hogeエラーをthrowする', () => {
632
- // テストの実装を書く
633
- expect(statusCode).equal("401");
634
- expect(message).equal("エラーメッセージ");
635
- });
636
- });
637
- });
638
- });
639
- ```
640
-
641
- ### NG
642
-
643
- ```jsx
644
-
645
- import { describe, expect, it } from 'vitest';
646
- import { context, target } from '../../helper/common-helper';
647
-
648
- // targetを使っていない
649
- describe('getBaseContactPoint', () => {
650
- describe('providerIdsに応じた連絡先を返す', () => {
651
- // contextを使わずitに条件を記載している
652
- it('providerIdsにLINEが含まれている場合、LINEの問い合わせ先を返す', () => {
653
- // テストの実装を書く
654
- });
655
- });
82
+ ```ts
83
+ // OK: 日本語で仕様を表し、actual を使う
84
+ it('ユーザー識別子が不正な場合は検証エラーを返すこと', async () => {
85
+ const actual = await sut.execute(input);
86
+ expect(actual).toEqual(error);
656
87
  });
657
88
  ```
658
-
659
- ### 構文解説
660
-
661
- - target..テスト対象のメソッド
662
- - describe…テスト対象メソッドのふるまい説明
663
- - (context…検証したいふるまいに前提条件があればcontextとして記載)
664
- - it…期待値
665
-
666
- ### 背景
667
-
668
- - テストコードは上から下に読みやすい形式で書きたいため
669
- - itに条件まで記載すると認知負荷が高まってしまう
670
- - テストコードをtargetやcontextの観点で構造化することで、各テストケースの目的や前提条件がより明確となり、全体の可読性と保守性が向上する