phasegate 0.134.0 → 0.135.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.
- package/CHANGELOG.md +11 -0
- package/bin/phasegate +2 -0
- package/docs/contracts/lesson-artifact.schema.json +62 -0
- package/docs/contracts/requirement-test-matrix.schema.json +62 -0
- package/docs/guide/layer-model.md +2 -2
- package/docs/principles/architecture-philosophy.md +26 -55
- package/docs/principles/model-routing.md +30 -165
- package/docs/principles/testing-rules.md +66 -648
- package/package.json +4 -3
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
- package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
- package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
- package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
- package/scripts/harness/validator-system/composition-root.ts +8 -2
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -4
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +11 -2
|
@@ -1,670 +1,88 @@
|
|
|
1
1
|
# テスト規約
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
```tsx
|
|
463
|
-
it('指定したスペースIDが存在しない場合は、404を返す', async () => {}
|
|
464
|
-
```
|
|
68
|
+
- 小さなドメインルール、値オブジェクト、サービス、UseCase の分岐を焦点化して検証する。
|
|
69
|
+
- 1 テスト 1 ルールを基本とし、境界値や異常系は分ける。
|
|
70
|
+
- リファクタリングで壊れないよう、内部実装ではなく外部から見えるふるまいを検証する。
|
|
465
71
|
|
|
466
|
-
|
|
72
|
+
## 禁止例
|
|
467
73
|
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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の観点で構造化することで、各テストケースの目的や前提条件がより明確となり、全体の可読性と保守性が向上する
|