@famgia/omnify-laravel 0.0.87 → 0.0.89
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/dist/{chunk-YVVAJA3T.js → chunk-V7LWJ6OM.js} +178 -12
- package/dist/chunk-V7LWJ6OM.js.map +1 -0
- package/dist/index.cjs +180 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +5 -1
- package/dist/plugin.cjs +176 -11
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +5 -5
- package/scripts/postinstall.js +29 -36
- package/stubs/ai-guides/claude-agents/architect.md.stub +150 -0
- package/stubs/ai-guides/claude-agents/developer.md.stub +190 -0
- package/stubs/ai-guides/claude-agents/reviewer.md.stub +134 -0
- package/stubs/ai-guides/claude-agents/tester.md.stub +196 -0
- package/stubs/ai-guides/claude-checklists/backend.md.stub +112 -0
- package/stubs/ai-guides/claude-omnify/antdesign-guide.md.stub +401 -0
- package/stubs/ai-guides/claude-omnify/config-guide.md.stub +253 -0
- package/stubs/ai-guides/claude-omnify/japan-guide.md.stub +186 -0
- package/stubs/ai-guides/claude-omnify/laravel-guide.md.stub +61 -0
- package/stubs/ai-guides/claude-omnify/schema-guide.md.stub +115 -0
- package/stubs/ai-guides/claude-omnify/typescript-guide.md.stub +310 -0
- package/stubs/ai-guides/claude-rules/naming.md.stub +364 -0
- package/stubs/ai-guides/claude-rules/performance.md.stub +251 -0
- package/stubs/ai-guides/claude-rules/security.md.stub +159 -0
- package/stubs/ai-guides/claude-workflows/bug-fix.md.stub +201 -0
- package/stubs/ai-guides/claude-workflows/code-review.md.stub +164 -0
- package/stubs/ai-guides/claude-workflows/new-feature.md.stub +327 -0
- package/stubs/ai-guides/cursor/laravel-controller.mdc.stub +391 -0
- package/stubs/ai-guides/cursor/laravel-request.mdc.stub +112 -0
- package/stubs/ai-guides/cursor/laravel-resource.mdc.stub +73 -0
- package/stubs/ai-guides/cursor/laravel-review.mdc.stub +69 -0
- package/stubs/ai-guides/cursor/laravel-testing.mdc.stub +138 -0
- package/stubs/ai-guides/cursor/laravel.mdc.stub +82 -0
- package/stubs/ai-guides/laravel/README.md.stub +59 -0
- package/stubs/ai-guides/laravel/architecture.md.stub +424 -0
- package/stubs/ai-guides/laravel/controller.md.stub +484 -0
- package/stubs/ai-guides/laravel/datetime.md.stub +334 -0
- package/stubs/ai-guides/laravel/openapi.md.stub +369 -0
- package/stubs/ai-guides/laravel/request.md.stub +450 -0
- package/stubs/ai-guides/laravel/resource.md.stub +516 -0
- package/stubs/ai-guides/laravel/service.md.stub +503 -0
- package/stubs/ai-guides/laravel/testing.md.stub +1504 -0
- package/ai-guides/laravel-guide.md +0 -461
- package/dist/chunk-YVVAJA3T.js.map +0 -1
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
# Testing Guide (PEST)
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Naming Conventions](./naming-conventions.md) | [Checklist](./checklist.md)
|
|
4
|
+
|
|
5
|
+
## Quick Start - How to Run Tests
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run ALL tests (from project root - uses Docker wrapper)
|
|
9
|
+
./artisan test
|
|
10
|
+
|
|
11
|
+
# Run specific test file
|
|
12
|
+
./artisan test --filter=UserControllerTest
|
|
13
|
+
|
|
14
|
+
# Run specific test method
|
|
15
|
+
./artisan test --filter="creates user with valid data"
|
|
16
|
+
|
|
17
|
+
# Run with verbose output
|
|
18
|
+
./artisan test -v
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Note:** The root `./artisan` script is a wrapper that runs `docker compose exec backend php artisan` automatically.
|
|
22
|
+
|
|
23
|
+
## Critical: Database Trait
|
|
24
|
+
|
|
25
|
+
**MUST use `RefreshDatabase` for SQLite in-memory testing:**
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
// ✅ CORRECT - Use RefreshDatabase
|
|
29
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
30
|
+
uses(RefreshDatabase::class);
|
|
31
|
+
|
|
32
|
+
// ❌ WRONG - DatabaseTransactions doesn't run migrations
|
|
33
|
+
// Will fail with "no such table" error!
|
|
34
|
+
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
35
|
+
uses(DatabaseTransactions::class);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Trait | Database | Behavior |
|
|
39
|
+
| ---------------------- | ---------------- | --------------------------------------------- |
|
|
40
|
+
| `RefreshDatabase` | SQLite in-memory | Runs migrations → truncates after each test |
|
|
41
|
+
| `DatabaseTransactions` | MySQL/PostgreSQL | Only wraps in transaction (tables must exist) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Overview
|
|
46
|
+
|
|
47
|
+
This project uses **PEST** for testing. Every API endpoint MUST have tests covering:
|
|
48
|
+
|
|
49
|
+
- **Normal cases (正常系)** - Happy path, expected behavior
|
|
50
|
+
- **Abnormal cases (異常系)** - Validation errors, edge cases
|
|
51
|
+
|
|
52
|
+
**Principle**: If you can't test it, you can't ship it.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Test Categories
|
|
57
|
+
|
|
58
|
+
| Category | Description | HTTP Codes |
|
|
59
|
+
| --------------------- | --------------------------------- | ------------- |
|
|
60
|
+
| **Normal (正常系)** | Happy path, expected behavior | 200, 201, 204 |
|
|
61
|
+
| **Abnormal (異常系)** | Validation errors, business rules | 422 |
|
|
62
|
+
| **Not Found** | Resource doesn't exist | 404 |
|
|
63
|
+
| **Auth Required** | Unauthenticated request | 401 |
|
|
64
|
+
| **Forbidden** | Unauthorized action | 403 |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Test Structure
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
tests/
|
|
72
|
+
├── Feature/
|
|
73
|
+
│ ├── Api/ # API endpoint tests
|
|
74
|
+
│ │ ├── UserControllerTest.php
|
|
75
|
+
│ │ └── PostControllerTest.php
|
|
76
|
+
│ └── Auth/ # Authentication flow tests
|
|
77
|
+
│ ├── LoginTest.php
|
|
78
|
+
│ └── RegistrationTest.php
|
|
79
|
+
├── Unit/
|
|
80
|
+
│ ├── Services/ # Service class tests
|
|
81
|
+
│ │ └── OrderServiceTest.php
|
|
82
|
+
│ ├── Models/ # Model tests (accessors, scopes)
|
|
83
|
+
│ │ └── UserTest.php
|
|
84
|
+
│ └── Rules/ # Custom validation rules
|
|
85
|
+
│ └── KatakanaRuleTest.php
|
|
86
|
+
└── Pest.php # PEST configuration
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Feature vs Unit Tests
|
|
92
|
+
|
|
93
|
+
### When to Use Feature Tests
|
|
94
|
+
|
|
95
|
+
**Feature tests** test the full HTTP request/response cycle.
|
|
96
|
+
|
|
97
|
+
```php
|
|
98
|
+
// ✅ Feature Test: Full HTTP cycle
|
|
99
|
+
// tests/Feature/Api/UserControllerTest.php
|
|
100
|
+
it('正常: creates user with valid data', function () {
|
|
101
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
102
|
+
|
|
103
|
+
$response->assertCreated();
|
|
104
|
+
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Use Feature tests for:**
|
|
109
|
+
- API endpoints (Controllers)
|
|
110
|
+
- Authentication flows (Login, Logout, Register)
|
|
111
|
+
- Middleware behavior
|
|
112
|
+
- Full request validation
|
|
113
|
+
- Database state verification
|
|
114
|
+
|
|
115
|
+
### When to Use Unit Tests
|
|
116
|
+
|
|
117
|
+
**Unit tests** test isolated classes/methods without HTTP.
|
|
118
|
+
|
|
119
|
+
```php
|
|
120
|
+
// ✅ Unit Test: Isolated logic
|
|
121
|
+
// tests/Unit/Services/OrderServiceTest.php
|
|
122
|
+
it('正常: calculates total with tax', function () {
|
|
123
|
+
$service = new OrderService();
|
|
124
|
+
|
|
125
|
+
$total = $service->calculateTotal(items: $items, taxRate: 0.1);
|
|
126
|
+
|
|
127
|
+
expect($total)->toBe(1100);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// tests/Unit/Models/UserTest.php
|
|
131
|
+
it('正常: returns full name', function () {
|
|
132
|
+
$user = new User([
|
|
133
|
+
'name_lastname' => '田中',
|
|
134
|
+
'name_firstname' => '太郎',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect($user->name_full_name)->toBe('田中 太郎');
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Use Unit tests for:**
|
|
142
|
+
- Service/Action classes
|
|
143
|
+
- Model accessors/mutators
|
|
144
|
+
- Model scopes
|
|
145
|
+
- Helper functions
|
|
146
|
+
- Custom validation rules
|
|
147
|
+
- Pure business logic
|
|
148
|
+
|
|
149
|
+
### Decision Matrix
|
|
150
|
+
|
|
151
|
+
| What to Test | Test Type | Location |
|
|
152
|
+
| --------------- | --------- | --------------------- |
|
|
153
|
+
| API endpoint | Feature | `Feature/Api/` |
|
|
154
|
+
| Login/Logout | Feature | `Feature/Auth/` |
|
|
155
|
+
| Middleware | Feature | `Feature/Middleware/` |
|
|
156
|
+
| Service class | Unit | `Unit/Services/` |
|
|
157
|
+
| Model accessor | Unit | `Unit/Models/` |
|
|
158
|
+
| Custom rule | Unit | `Unit/Rules/` |
|
|
159
|
+
| Helper function | Unit | `Unit/Helpers/` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Mocking & Faking
|
|
164
|
+
|
|
165
|
+
### Fake Mail
|
|
166
|
+
|
|
167
|
+
```php
|
|
168
|
+
use Illuminate\Support\Facades\Mail;
|
|
169
|
+
use App\Mail\WelcomeEmail;
|
|
170
|
+
|
|
171
|
+
it('正常: sends welcome email on registration', function () {
|
|
172
|
+
Mail::fake();
|
|
173
|
+
|
|
174
|
+
$this->postJson('/api/register', validUserData())
|
|
175
|
+
->assertCreated();
|
|
176
|
+
|
|
177
|
+
Mail::assertSent(WelcomeEmail::class, function ($mail) {
|
|
178
|
+
return $mail->hasTo('test@example.com');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('異常: does not send email on validation failure', function () {
|
|
183
|
+
Mail::fake();
|
|
184
|
+
|
|
185
|
+
$this->postJson('/api/register', [])
|
|
186
|
+
->assertUnprocessable();
|
|
187
|
+
|
|
188
|
+
Mail::assertNothingSent();
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Fake Queue
|
|
193
|
+
|
|
194
|
+
```php
|
|
195
|
+
use Illuminate\Support\Facades\Queue;
|
|
196
|
+
use App\Jobs\ProcessOrder;
|
|
197
|
+
|
|
198
|
+
it('正常: dispatches job on order creation', function () {
|
|
199
|
+
Queue::fake();
|
|
200
|
+
|
|
201
|
+
$this->postJson('/api/orders', validOrderData())
|
|
202
|
+
->assertCreated();
|
|
203
|
+
|
|
204
|
+
Queue::assertPushed(ProcessOrder::class);
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Fake Storage
|
|
209
|
+
|
|
210
|
+
```php
|
|
211
|
+
use Illuminate\Support\Facades\Storage;
|
|
212
|
+
use Illuminate\Http\UploadedFile;
|
|
213
|
+
|
|
214
|
+
it('正常: uploads avatar', function () {
|
|
215
|
+
Storage::fake('public');
|
|
216
|
+
|
|
217
|
+
$file = UploadedFile::fake()->image('avatar.jpg');
|
|
218
|
+
|
|
219
|
+
$this->postJson('/api/users/avatar', ['avatar' => $file])
|
|
220
|
+
->assertOk();
|
|
221
|
+
|
|
222
|
+
Storage::disk('public')->assertExists('avatars/' . $file->hashName());
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Fake Notification
|
|
227
|
+
|
|
228
|
+
```php
|
|
229
|
+
use Illuminate\Support\Facades\Notification;
|
|
230
|
+
use App\Notifications\OrderShipped;
|
|
231
|
+
|
|
232
|
+
it('正常: notifies user when order ships', function () {
|
|
233
|
+
Notification::fake();
|
|
234
|
+
|
|
235
|
+
$order = Order::factory()->create();
|
|
236
|
+
|
|
237
|
+
$this->postJson("/api/orders/{$order->id}/ship")
|
|
238
|
+
->assertOk();
|
|
239
|
+
|
|
240
|
+
Notification::assertSentTo($order->user, OrderShipped::class);
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Fake HTTP (External APIs)
|
|
245
|
+
|
|
246
|
+
```php
|
|
247
|
+
use Illuminate\Support\Facades\Http;
|
|
248
|
+
|
|
249
|
+
it('正常: fetches data from external API', function () {
|
|
250
|
+
Http::fake([
|
|
251
|
+
'api.example.com/*' => Http::response(['data' => 'value'], 200),
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
$response = $this->getJson('/api/external-data');
|
|
255
|
+
|
|
256
|
+
$response->assertOk()
|
|
257
|
+
->assertJsonPath('data', 'value');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('異常: handles external API failure', function () {
|
|
261
|
+
Http::fake([
|
|
262
|
+
'api.example.com/*' => Http::response([], 500),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
$response = $this->getJson('/api/external-data');
|
|
266
|
+
|
|
267
|
+
$response->assertServiceUnavailable();
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Mock Service
|
|
272
|
+
|
|
273
|
+
```php
|
|
274
|
+
use App\Services\PaymentService;
|
|
275
|
+
|
|
276
|
+
it('正常: processes payment', function () {
|
|
277
|
+
$mock = Mockery::mock(PaymentService::class);
|
|
278
|
+
$mock->shouldReceive('charge')
|
|
279
|
+
->once()
|
|
280
|
+
->with(1000, 'tok_visa')
|
|
281
|
+
->andReturn(true);
|
|
282
|
+
|
|
283
|
+
$this->app->instance(PaymentService::class, $mock);
|
|
284
|
+
|
|
285
|
+
$this->postJson('/api/orders/pay', ['token' => 'tok_visa'])
|
|
286
|
+
->assertOk();
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Authentication Tests
|
|
293
|
+
|
|
294
|
+
### Login Tests
|
|
295
|
+
|
|
296
|
+
```php
|
|
297
|
+
// tests/Feature/Auth/LoginTest.php
|
|
298
|
+
|
|
299
|
+
describe('POST /api/login', function () {
|
|
300
|
+
|
|
301
|
+
it('正常: logs in with valid credentials', function () {
|
|
302
|
+
$user = User::factory()->create([
|
|
303
|
+
'email' => 'test@example.com',
|
|
304
|
+
'password' => 'password123',
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
$response = $this->postJson('/api/login', [
|
|
308
|
+
'email' => 'test@example.com',
|
|
309
|
+
'password' => 'password123',
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
$response->assertOk()
|
|
313
|
+
->assertJsonStructure(['token', 'user']);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('異常: fails with wrong password', function () {
|
|
317
|
+
User::factory()->create([
|
|
318
|
+
'email' => 'test@example.com',
|
|
319
|
+
'password' => 'password123',
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
$response = $this->postJson('/api/login', [
|
|
323
|
+
'email' => 'test@example.com',
|
|
324
|
+
'password' => 'wrongpassword',
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
$response->assertUnauthorized();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('異常: fails with nonexistent email', function () {
|
|
331
|
+
$response = $this->postJson('/api/login', [
|
|
332
|
+
'email' => 'notexist@example.com',
|
|
333
|
+
'password' => 'password123',
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
$response->assertUnauthorized();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Logout Tests
|
|
342
|
+
|
|
343
|
+
```php
|
|
344
|
+
describe('POST /api/logout', function () {
|
|
345
|
+
|
|
346
|
+
it('正常: logs out authenticated user', function () {
|
|
347
|
+
$user = User::factory()->create();
|
|
348
|
+
|
|
349
|
+
$response = $this->actingAs($user)
|
|
350
|
+
->postJson('/api/logout');
|
|
351
|
+
|
|
352
|
+
$response->assertNoContent();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('異常: returns 401 when not authenticated', function () {
|
|
356
|
+
$response = $this->postJson('/api/logout');
|
|
357
|
+
|
|
358
|
+
$response->assertUnauthorized();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Protected Route Tests
|
|
364
|
+
|
|
365
|
+
```php
|
|
366
|
+
describe('Protected routes', function () {
|
|
367
|
+
|
|
368
|
+
it('正常: allows authenticated user', function () {
|
|
369
|
+
$user = User::factory()->create();
|
|
370
|
+
|
|
371
|
+
$response = $this->actingAs($user)
|
|
372
|
+
->getJson('/api/me');
|
|
373
|
+
|
|
374
|
+
$response->assertOk()
|
|
375
|
+
->assertJsonPath('data.id', $user->id);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('異常: returns 401 without token', function () {
|
|
379
|
+
$response = $this->getJson('/api/me');
|
|
380
|
+
|
|
381
|
+
$response->assertUnauthorized();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('異常: returns 401 with invalid token', function () {
|
|
385
|
+
$response = $this->getJson('/api/me', [
|
|
386
|
+
'Authorization' => 'Bearer invalid_token',
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
$response->assertUnauthorized();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Token Refresh Tests (if applicable)
|
|
395
|
+
|
|
396
|
+
```php
|
|
397
|
+
describe('POST /api/refresh', function () {
|
|
398
|
+
|
|
399
|
+
it('正常: refreshes token', function () {
|
|
400
|
+
$user = User::factory()->create();
|
|
401
|
+
$token = $user->createToken('test')->plainTextToken;
|
|
402
|
+
|
|
403
|
+
$response = $this->withHeader('Authorization', "Bearer {$token}")
|
|
404
|
+
->postJson('/api/refresh');
|
|
405
|
+
|
|
406
|
+
$response->assertOk()
|
|
407
|
+
->assertJsonStructure(['token']);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Middleware Tests
|
|
415
|
+
|
|
416
|
+
### Rate Limiting
|
|
417
|
+
|
|
418
|
+
```php
|
|
419
|
+
describe('Rate limiting', function () {
|
|
420
|
+
|
|
421
|
+
it('異常: returns 429 when rate limit exceeded', function () {
|
|
422
|
+
$user = User::factory()->create();
|
|
423
|
+
|
|
424
|
+
// Hit the endpoint many times
|
|
425
|
+
for ($i = 0; $i < 60; $i++) {
|
|
426
|
+
$this->actingAs($user)->getJson('/api/users');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Next request should be rate limited
|
|
430
|
+
$response = $this->actingAs($user)->getJson('/api/users');
|
|
431
|
+
|
|
432
|
+
$response->assertStatus(429);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### CORS (if custom implementation)
|
|
438
|
+
|
|
439
|
+
```php
|
|
440
|
+
describe('CORS', function () {
|
|
441
|
+
|
|
442
|
+
it('正常: includes CORS headers', function () {
|
|
443
|
+
$response = $this->getJson('/api/users');
|
|
444
|
+
|
|
445
|
+
$response->assertHeader('Access-Control-Allow-Origin');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('正常: handles preflight OPTIONS request', function () {
|
|
449
|
+
$response = $this->options('/api/users', [], [
|
|
450
|
+
'Origin' => 'http://localhost:3000',
|
|
451
|
+
'Access-Control-Request-Method' => 'POST',
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
$response->assertOk()
|
|
455
|
+
->assertHeader('Access-Control-Allow-Methods');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Custom Middleware
|
|
461
|
+
|
|
462
|
+
```php
|
|
463
|
+
// Example: Admin only middleware
|
|
464
|
+
describe('Admin middleware', function () {
|
|
465
|
+
|
|
466
|
+
it('正常: allows admin user', function () {
|
|
467
|
+
$admin = User::factory()->create(['role' => 'admin']);
|
|
468
|
+
|
|
469
|
+
$response = $this->actingAs($admin)
|
|
470
|
+
->getJson('/api/admin/dashboard');
|
|
471
|
+
|
|
472
|
+
$response->assertOk();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('異常: returns 403 for non-admin', function () {
|
|
476
|
+
$user = User::factory()->create(['role' => 'user']);
|
|
477
|
+
|
|
478
|
+
$response = $this->actingAs($user)
|
|
479
|
+
->getJson('/api/admin/dashboard');
|
|
480
|
+
|
|
481
|
+
$response->assertForbidden();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Naming Conventions
|
|
489
|
+
|
|
490
|
+
> **See:** [Naming Conventions - Test Naming](./naming-conventions.md#test-naming-pest) for complete naming rules
|
|
491
|
+
|
|
492
|
+
### Quick Reference
|
|
493
|
+
|
|
494
|
+
| Category | Prefix | Example |
|
|
495
|
+
| --------------------- | ------- | ----------------------------------------------------- |
|
|
496
|
+
| **正常系 (Normal)** | `正常:` | `it('正常: creates user with valid data')` |
|
|
497
|
+
| **異常系 (Abnormal)** | `異常:` | `it('異常: fails to create user with invalid email')` |
|
|
498
|
+
|
|
499
|
+
### Test File Names
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
{Model}ControllerTest.php # Feature tests for API
|
|
503
|
+
{Model}Test.php # Unit tests for Model
|
|
504
|
+
{Service}Test.php # Unit tests for Service
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Test Coverage Matrix
|
|
510
|
+
|
|
511
|
+
### CRUD Endpoint Coverage
|
|
512
|
+
|
|
513
|
+
| Endpoint | Normal (正常系) | Abnormal (異常系) |
|
|
514
|
+
| ----------------------------------------- | ----------------------- | ----------------------- |
|
|
515
|
+
| **GET /api/{resource}** (index) | Returns paginated list | Empty list when no data |
|
|
516
|
+
| | Filters by search | Invalid query params |
|
|
517
|
+
| | Sorts by field | Invalid sort field |
|
|
518
|
+
| | Pagination works | |
|
|
519
|
+
| **POST /api/{resource}** (store) | Creates with valid data | Missing required fields |
|
|
520
|
+
| | Returns 201 + resource | Invalid field format |
|
|
521
|
+
| | | Duplicate unique field |
|
|
522
|
+
| | | Validation errors (422) |
|
|
523
|
+
| **GET /api/{resource}/{id}** (show) | Returns resource | Not found (404) |
|
|
524
|
+
| | | Invalid ID format |
|
|
525
|
+
| **PUT /api/{resource}/{id}** (update) | Updates with valid data | Not found (404) |
|
|
526
|
+
| | Partial update works | Invalid data (422) |
|
|
527
|
+
| | | Duplicate unique field |
|
|
528
|
+
| **DELETE /api/{resource}/{id}** (destroy) | Deletes resource | Not found (404) |
|
|
529
|
+
| | Returns 204 | |
|
|
530
|
+
|
|
531
|
+
### Field Validation Coverage
|
|
532
|
+
|
|
533
|
+
| Field Type | Normal (正常系) | Abnormal (異常系) |
|
|
534
|
+
| ------------ | ---------------- | ------------------------ |
|
|
535
|
+
| **required** | Field present | Field missing |
|
|
536
|
+
| | | Field empty |
|
|
537
|
+
| | | Field null |
|
|
538
|
+
| **string** | Valid string | Non-string type |
|
|
539
|
+
| | | Too long (max) |
|
|
540
|
+
| **email** | Valid email | Invalid format |
|
|
541
|
+
| | | Missing @ |
|
|
542
|
+
| **unique** | New value | Duplicate value |
|
|
543
|
+
| **min:N** | At limit | Below limit |
|
|
544
|
+
| **max:N** | At limit | Above limit |
|
|
545
|
+
| **integer** | Valid integer | Non-integer |
|
|
546
|
+
| | | Negative (if applicable) |
|
|
547
|
+
| **date** | Valid date | Invalid format |
|
|
548
|
+
| **enum** | Valid option | Invalid option |
|
|
549
|
+
| **regex** | Matching pattern | Non-matching pattern |
|
|
550
|
+
|
|
551
|
+
### Authentication & Authorization Coverage
|
|
552
|
+
|
|
553
|
+
| Scenario | Normal (正常系) | Abnormal (異常系) |
|
|
554
|
+
| -------------------- | ----------------------- | --------------------- |
|
|
555
|
+
| **No auth required** | Returns data | - |
|
|
556
|
+
| **Auth required** | Authenticated → success | Unauthenticated → 401 |
|
|
557
|
+
| **Owner only** | Owner → success | Non-owner → 403 |
|
|
558
|
+
| **Admin only** | Admin → success | Non-admin → 403 |
|
|
559
|
+
| **Role-based** | Has role → success | Missing role → 403 |
|
|
560
|
+
|
|
561
|
+
### Japanese Field Validation Coverage
|
|
562
|
+
|
|
563
|
+
| Field | Normal (正常系) | Abnormal (異常系) |
|
|
564
|
+
| --------------------- | --------------------- | ------------------------------ |
|
|
565
|
+
| `name_lastname` | Valid kanji/hiragana | Empty, too long (>50) |
|
|
566
|
+
| `name_firstname` | Valid kanji/hiragana | Empty, too long (>50) |
|
|
567
|
+
| `name_kana_lastname` | Valid katakana | Hiragana, kanji, romaji |
|
|
568
|
+
| `name_kana_firstname` | Valid katakana | Hiragana, kanji, romaji |
|
|
569
|
+
| `phone` | Valid Japanese format | Invalid format, too short/long |
|
|
570
|
+
| `postal_code` | 7 digits (no hyphen) | With hyphen, wrong length |
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## PEST Test Template (CRUD)
|
|
575
|
+
|
|
576
|
+
```php
|
|
577
|
+
<?php
|
|
578
|
+
|
|
579
|
+
use App\Models\User;
|
|
580
|
+
|
|
581
|
+
// Helper function for valid user data
|
|
582
|
+
function validUserData(array $overrides = []): array
|
|
583
|
+
{
|
|
584
|
+
return array_merge([
|
|
585
|
+
'name_lastname' => 'Tanaka',
|
|
586
|
+
'name_firstname' => 'Taro',
|
|
587
|
+
'name_kana_lastname' => 'タナカ',
|
|
588
|
+
'name_kana_firstname' => 'タロウ',
|
|
589
|
+
'email' => 'tanaka@example.com',
|
|
590
|
+
'password' => 'password123',
|
|
591
|
+
], $overrides);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// =============================================================================
|
|
595
|
+
// INDEX (GET /api/users)
|
|
596
|
+
// =============================================================================
|
|
597
|
+
|
|
598
|
+
describe('GET /api/users', function () {
|
|
599
|
+
|
|
600
|
+
// Normal cases (正常系)
|
|
601
|
+
|
|
602
|
+
it('正常: returns paginated users', function () {
|
|
603
|
+
User::factory()->count(15)->create();
|
|
604
|
+
|
|
605
|
+
$response = $this->getJson('/api/users');
|
|
606
|
+
|
|
607
|
+
$response->assertOk()
|
|
608
|
+
->assertJsonStructure([
|
|
609
|
+
'data' => [['id', 'name_lastname', 'name_firstname', 'email']],
|
|
610
|
+
'meta' => ['current_page', 'last_page', 'per_page', 'total'],
|
|
611
|
+
])
|
|
612
|
+
->assertJsonCount(10, 'data');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('正常: filters users by search term', function () {
|
|
616
|
+
User::factory()->create(['name_lastname' => 'Tanaka']);
|
|
617
|
+
User::factory()->create(['name_lastname' => 'Yamada']);
|
|
618
|
+
|
|
619
|
+
$response = $this->getJson('/api/users?search=Tanaka');
|
|
620
|
+
|
|
621
|
+
$response->assertOk()
|
|
622
|
+
->assertJsonCount(1, 'data');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('正常: sorts users by specified field', function () {
|
|
626
|
+
User::factory()->create(['name_lastname' => 'B']);
|
|
627
|
+
User::factory()->create(['name_lastname' => 'A']);
|
|
628
|
+
|
|
629
|
+
$response = $this->getJson('/api/users?sort_by=name_lastname&sort_order=asc');
|
|
630
|
+
|
|
631
|
+
$response->assertOk();
|
|
632
|
+
expect($response->json('data.0.name_lastname'))->toBe('A');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('正常: paginates with custom per_page', function () {
|
|
636
|
+
User::factory()->count(10)->create();
|
|
637
|
+
|
|
638
|
+
$response = $this->getJson('/api/users?per_page=5');
|
|
639
|
+
|
|
640
|
+
$response->assertOk()
|
|
641
|
+
->assertJsonCount(5, 'data')
|
|
642
|
+
->assertJsonPath('meta.per_page', 5);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Abnormal cases (異常系)
|
|
646
|
+
|
|
647
|
+
it('異常: returns empty array when no users exist', function () {
|
|
648
|
+
$response = $this->getJson('/api/users');
|
|
649
|
+
|
|
650
|
+
$response->assertOk()
|
|
651
|
+
->assertJsonCount(0, 'data');
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// =============================================================================
|
|
656
|
+
// STORE (POST /api/users)
|
|
657
|
+
// =============================================================================
|
|
658
|
+
|
|
659
|
+
describe('POST /api/users', function () {
|
|
660
|
+
|
|
661
|
+
// Normal cases (正常系)
|
|
662
|
+
|
|
663
|
+
it('正常: creates user with valid data', function () {
|
|
664
|
+
$data = validUserData();
|
|
665
|
+
|
|
666
|
+
$response = $this->postJson('/api/users', $data);
|
|
667
|
+
|
|
668
|
+
$response->assertCreated()
|
|
669
|
+
->assertJsonPath('data.email', 'tanaka@example.com');
|
|
670
|
+
|
|
671
|
+
$this->assertDatabaseHas('users', ['email' => 'tanaka@example.com']);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Abnormal cases (異常系)
|
|
675
|
+
|
|
676
|
+
it('異常: fails with missing required fields', function () {
|
|
677
|
+
$response = $this->postJson('/api/users', []);
|
|
678
|
+
|
|
679
|
+
$response->assertUnprocessable()
|
|
680
|
+
->assertJsonValidationErrors([
|
|
681
|
+
'name_lastname',
|
|
682
|
+
'name_firstname',
|
|
683
|
+
'name_kana_lastname',
|
|
684
|
+
'name_kana_firstname',
|
|
685
|
+
'email',
|
|
686
|
+
'password',
|
|
687
|
+
]);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('異常: fails with invalid email format', function () {
|
|
691
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
692
|
+
'email' => 'invalid-email',
|
|
693
|
+
]));
|
|
694
|
+
|
|
695
|
+
$response->assertUnprocessable()
|
|
696
|
+
->assertJsonValidationErrors(['email']);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('異常: fails with duplicate email', function () {
|
|
700
|
+
User::factory()->create(['email' => 'existing@example.com']);
|
|
701
|
+
|
|
702
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
703
|
+
'email' => 'existing@example.com',
|
|
704
|
+
]));
|
|
705
|
+
|
|
706
|
+
$response->assertUnprocessable()
|
|
707
|
+
->assertJsonValidationErrors(['email']);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('異常: fails with password too short', function () {
|
|
711
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
712
|
+
'password' => '123',
|
|
713
|
+
]));
|
|
714
|
+
|
|
715
|
+
$response->assertUnprocessable()
|
|
716
|
+
->assertJsonValidationErrors(['password']);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// =============================================================================
|
|
721
|
+
// SHOW (GET /api/users/{id})
|
|
722
|
+
// =============================================================================
|
|
723
|
+
|
|
724
|
+
describe('GET /api/users/{id}', function () {
|
|
725
|
+
|
|
726
|
+
// Normal cases (正常系)
|
|
727
|
+
|
|
728
|
+
it('正常: returns user by id', function () {
|
|
729
|
+
$user = User::factory()->create();
|
|
730
|
+
|
|
731
|
+
$response = $this->getJson("/api/users/{$user->id}");
|
|
732
|
+
|
|
733
|
+
$response->assertOk()
|
|
734
|
+
->assertJsonPath('data.id', $user->id)
|
|
735
|
+
->assertJsonPath('data.email', $user->email);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Abnormal cases (異常系)
|
|
739
|
+
|
|
740
|
+
it('異常: returns 404 for nonexistent user', function () {
|
|
741
|
+
$response = $this->getJson('/api/users/99999');
|
|
742
|
+
|
|
743
|
+
$response->assertNotFound();
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// =============================================================================
|
|
748
|
+
// UPDATE (PUT /api/users/{id})
|
|
749
|
+
// =============================================================================
|
|
750
|
+
|
|
751
|
+
describe('PUT /api/users/{id}', function () {
|
|
752
|
+
|
|
753
|
+
// Normal cases (正常系)
|
|
754
|
+
|
|
755
|
+
it('正常: updates user with valid data', function () {
|
|
756
|
+
$user = User::factory()->create();
|
|
757
|
+
|
|
758
|
+
$response = $this->putJson("/api/users/{$user->id}", [
|
|
759
|
+
'name_lastname' => 'Yamada',
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
$response->assertOk()
|
|
763
|
+
->assertJsonPath('data.name_lastname', 'Yamada');
|
|
764
|
+
|
|
765
|
+
$this->assertDatabaseHas('users', [
|
|
766
|
+
'id' => $user->id,
|
|
767
|
+
'name_lastname' => 'Yamada',
|
|
768
|
+
]);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('正常: allows partial update', function () {
|
|
772
|
+
$user = User::factory()->create(['name_lastname' => 'Tanaka']);
|
|
773
|
+
|
|
774
|
+
$response = $this->putJson("/api/users/{$user->id}", [
|
|
775
|
+
'name_firstname' => 'Jiro',
|
|
776
|
+
]);
|
|
777
|
+
|
|
778
|
+
$response->assertOk();
|
|
779
|
+
$this->assertDatabaseHas('users', [
|
|
780
|
+
'id' => $user->id,
|
|
781
|
+
'name_lastname' => 'Tanaka', // Unchanged
|
|
782
|
+
'name_firstname' => 'Jiro', // Updated
|
|
783
|
+
]);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('正常: allows keeping same email', function () {
|
|
787
|
+
$user = User::factory()->create(['email' => 'same@example.com']);
|
|
788
|
+
|
|
789
|
+
$response = $this->putJson("/api/users/{$user->id}", [
|
|
790
|
+
'email' => 'same@example.com',
|
|
791
|
+
]);
|
|
792
|
+
|
|
793
|
+
$response->assertOk();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Abnormal cases (異常系)
|
|
797
|
+
|
|
798
|
+
it('異常: returns 404 for nonexistent user', function () {
|
|
799
|
+
$response = $this->putJson('/api/users/99999', [
|
|
800
|
+
'name_lastname' => 'Yamada',
|
|
801
|
+
]);
|
|
802
|
+
|
|
803
|
+
$response->assertNotFound();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('異常: fails with duplicate email', function () {
|
|
807
|
+
$user1 = User::factory()->create(['email' => 'user1@example.com']);
|
|
808
|
+
User::factory()->create(['email' => 'user2@example.com']);
|
|
809
|
+
|
|
810
|
+
$response = $this->putJson("/api/users/{$user1->id}", [
|
|
811
|
+
'email' => 'user2@example.com',
|
|
812
|
+
]);
|
|
813
|
+
|
|
814
|
+
$response->assertUnprocessable()
|
|
815
|
+
->assertJsonValidationErrors(['email']);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// =============================================================================
|
|
820
|
+
// DESTROY (DELETE /api/users/{id})
|
|
821
|
+
// =============================================================================
|
|
822
|
+
|
|
823
|
+
describe('DELETE /api/users/{id}', function () {
|
|
824
|
+
|
|
825
|
+
// Normal cases (正常系)
|
|
826
|
+
|
|
827
|
+
it('正常: deletes user', function () {
|
|
828
|
+
$user = User::factory()->create();
|
|
829
|
+
|
|
830
|
+
$response = $this->deleteJson("/api/users/{$user->id}");
|
|
831
|
+
|
|
832
|
+
$response->assertNoContent();
|
|
833
|
+
$this->assertDatabaseMissing('users', ['id' => $user->id]);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Abnormal cases (異常系)
|
|
837
|
+
|
|
838
|
+
it('異常: returns 404 for nonexistent user', function () {
|
|
839
|
+
$response = $this->deleteJson('/api/users/99999');
|
|
840
|
+
|
|
841
|
+
$response->assertNotFound();
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// =============================================================================
|
|
846
|
+
// AUTHENTICATION (401)
|
|
847
|
+
// =============================================================================
|
|
848
|
+
|
|
849
|
+
// Uncomment when auth is required
|
|
850
|
+
// describe('Authentication', function () {
|
|
851
|
+
// it('異常: returns 401 when unauthenticated for index', function () {
|
|
852
|
+
// $response = $this->getJson('/api/users');
|
|
853
|
+
// $response->assertUnauthorized();
|
|
854
|
+
// });
|
|
855
|
+
//
|
|
856
|
+
// it('異常: returns 401 when unauthenticated for store', function () {
|
|
857
|
+
// $response = $this->postJson('/api/users', validUserData());
|
|
858
|
+
// $response->assertUnauthorized();
|
|
859
|
+
// });
|
|
860
|
+
// });
|
|
861
|
+
|
|
862
|
+
// =============================================================================
|
|
863
|
+
// AUTHORIZATION (403)
|
|
864
|
+
// =============================================================================
|
|
865
|
+
|
|
866
|
+
// Uncomment when authorization is required
|
|
867
|
+
// describe('Authorization', function () {
|
|
868
|
+
// it('異常: returns 403 when updating other user', function () {
|
|
869
|
+
// $user = User::factory()->create();
|
|
870
|
+
// $otherUser = User::factory()->create();
|
|
871
|
+
//
|
|
872
|
+
// $this->actingAs($otherUser);
|
|
873
|
+
//
|
|
874
|
+
// $response = $this->putJson("/api/users/{$user->id}", [
|
|
875
|
+
// 'name_lastname' => 'Yamada',
|
|
876
|
+
// ]);
|
|
877
|
+
//
|
|
878
|
+
// $response->assertForbidden();
|
|
879
|
+
// });
|
|
880
|
+
//
|
|
881
|
+
// it('異常: returns 403 when deleting without admin role', function () {
|
|
882
|
+
// $user = User::factory()->create();
|
|
883
|
+
// $normalUser = User::factory()->create();
|
|
884
|
+
//
|
|
885
|
+
// $this->actingAs($normalUser);
|
|
886
|
+
//
|
|
887
|
+
// $response = $this->deleteJson("/api/users/{$user->id}");
|
|
888
|
+
//
|
|
889
|
+
// $response->assertForbidden();
|
|
890
|
+
// });
|
|
891
|
+
// });
|
|
892
|
+
|
|
893
|
+
// =============================================================================
|
|
894
|
+
// JAPANESE FIELD VALIDATION
|
|
895
|
+
// =============================================================================
|
|
896
|
+
|
|
897
|
+
describe('Japanese field validation', function () {
|
|
898
|
+
|
|
899
|
+
it('異常: fails with hiragana in kana field', function () {
|
|
900
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
901
|
+
'name_kana_lastname' => 'たなか', // Hiragana - should be Katakana
|
|
902
|
+
]));
|
|
903
|
+
|
|
904
|
+
$response->assertUnprocessable()
|
|
905
|
+
->assertJsonValidationErrors(['name_kana_lastname']);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('異常: fails with romaji in kana field', function () {
|
|
909
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
910
|
+
'name_kana_lastname' => 'Tanaka', // Romaji - should be Katakana
|
|
911
|
+
]));
|
|
912
|
+
|
|
913
|
+
$response->assertUnprocessable()
|
|
914
|
+
->assertJsonValidationErrors(['name_kana_lastname']);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('正常: accepts valid katakana', function () {
|
|
918
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
919
|
+
'name_kana_lastname' => 'タナカ',
|
|
920
|
+
'name_kana_firstname' => 'タロウ',
|
|
921
|
+
]));
|
|
922
|
+
|
|
923
|
+
$response->assertCreated();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('正常: accepts katakana with long vowel mark', function () {
|
|
927
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
928
|
+
'name_kana_lastname' => 'サトー',
|
|
929
|
+
'name_kana_firstname' => 'ユーコ',
|
|
930
|
+
]));
|
|
931
|
+
|
|
932
|
+
$response->assertCreated();
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('異常: fails with name exceeding max length', function () {
|
|
936
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
937
|
+
'name_lastname' => str_repeat('a', 51), // Over 50 chars
|
|
938
|
+
]));
|
|
939
|
+
|
|
940
|
+
$response->assertUnprocessable()
|
|
941
|
+
->assertJsonValidationErrors(['name_lastname']);
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## Test Checklist
|
|
949
|
+
|
|
950
|
+
### Per Endpoint
|
|
951
|
+
|
|
952
|
+
- [ ] **Normal cases (正常系)**
|
|
953
|
+
- [ ] Happy path with valid data
|
|
954
|
+
- [ ] All optional features work (search, sort, pagination)
|
|
955
|
+
- [ ] Response structure is correct
|
|
956
|
+
- [ ] Database state is correct
|
|
957
|
+
|
|
958
|
+
- [ ] **Abnormal cases (異常系)**
|
|
959
|
+
- [ ] 404 Not Found for missing resource
|
|
960
|
+
- [ ] 422 Validation Error for invalid data
|
|
961
|
+
- [ ] All required fields checked
|
|
962
|
+
- [ ] All format validations checked
|
|
963
|
+
- [ ] Unique constraints checked
|
|
964
|
+
|
|
965
|
+
### Per Field (Validation)
|
|
966
|
+
|
|
967
|
+
For each field in FormRequest:
|
|
968
|
+
|
|
969
|
+
- [ ] **Present & valid** → Success
|
|
970
|
+
- [ ] **Missing (if required)** → 422
|
|
971
|
+
- [ ] **Empty string** → 422 (if required)
|
|
972
|
+
- [ ] **Invalid format** → 422
|
|
973
|
+
- [ ] **Exceeds max length** → 422
|
|
974
|
+
- [ ] **Below min length** → 422
|
|
975
|
+
- [ ] **Duplicate (if unique)** → 422
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## Testing Database Setup
|
|
980
|
+
|
|
981
|
+
Tests use a separate database `omnify_testing` (auto-created by Docker).
|
|
982
|
+
|
|
983
|
+
**Files:**
|
|
984
|
+
- `docker/mysql/init/01-create-testing-db.sql` - Creates testing database on Docker init
|
|
985
|
+
- `backend/.env.testing` - Testing environment config (generated by `npm run setup`)
|
|
986
|
+
|
|
987
|
+
**Key differences from `.env`:**
|
|
988
|
+
|
|
989
|
+
| Setting | `.env` (dev) | `.env.testing` |
|
|
990
|
+
| ------------------ | ------------ | ---------------- |
|
|
991
|
+
| `DB_DATABASE` | `omnify` | `omnify_testing` |
|
|
992
|
+
| `SESSION_DRIVER` | `cookie` | `array` |
|
|
993
|
+
| `CACHE_STORE` | `file` | `array` |
|
|
994
|
+
| `QUEUE_CONNECTION` | `sync` | `sync` |
|
|
995
|
+
| `MAIL_MAILER` | `smtp` | `array` |
|
|
996
|
+
| `BCRYPT_ROUNDS` | `12` | `4` (faster) |
|
|
997
|
+
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
## Running Tests
|
|
1001
|
+
|
|
1002
|
+
```bash
|
|
1003
|
+
# Run all tests (from project root)
|
|
1004
|
+
./artisan test
|
|
1005
|
+
|
|
1006
|
+
# Run specific test file
|
|
1007
|
+
./artisan test --filter=UserControllerTest
|
|
1008
|
+
|
|
1009
|
+
# Run specific describe block
|
|
1010
|
+
./artisan test --filter="GET /api/users"
|
|
1011
|
+
|
|
1012
|
+
# Run specific test
|
|
1013
|
+
./artisan test --filter="creates user with valid data"
|
|
1014
|
+
|
|
1015
|
+
# Run with coverage
|
|
1016
|
+
./artisan test --coverage
|
|
1017
|
+
|
|
1018
|
+
# Run in parallel
|
|
1019
|
+
./artisan test --parallel
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### First Time Setup
|
|
1023
|
+
|
|
1024
|
+
If testing database doesn't exist:
|
|
1025
|
+
|
|
1026
|
+
```bash
|
|
1027
|
+
# Option 1: Recreate Docker containers (recommended)
|
|
1028
|
+
docker compose down -v
|
|
1029
|
+
docker compose up -d
|
|
1030
|
+
|
|
1031
|
+
# Option 2: Create manually
|
|
1032
|
+
docker compose exec mysql mysql -uroot -proot -e "CREATE DATABASE omnify_testing; GRANT ALL ON omnify_testing.* TO 'omnify'@'%';"
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## PEST Assertions Reference
|
|
1038
|
+
|
|
1039
|
+
### HTTP Assertions
|
|
1040
|
+
|
|
1041
|
+
```php
|
|
1042
|
+
$response->assertOk(); // 200
|
|
1043
|
+
$response->assertCreated(); // 201
|
|
1044
|
+
$response->assertNoContent(); // 204
|
|
1045
|
+
$response->assertNotFound(); // 404
|
|
1046
|
+
$response->assertUnprocessable(); // 422
|
|
1047
|
+
$response->assertUnauthorized(); // 401
|
|
1048
|
+
$response->assertForbidden(); // 403
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### JSON Assertions
|
|
1052
|
+
|
|
1053
|
+
```php
|
|
1054
|
+
$response->assertJsonStructure(['data' => ['id', 'email']]);
|
|
1055
|
+
$response->assertJsonPath('data.email', 'test@example.com');
|
|
1056
|
+
$response->assertJsonCount(10, 'data');
|
|
1057
|
+
$response->assertJsonValidationErrors(['email', 'password']);
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### Database Assertions
|
|
1061
|
+
|
|
1062
|
+
```php
|
|
1063
|
+
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
|
|
1064
|
+
$this->assertDatabaseMissing('users', ['id' => $user->id]);
|
|
1065
|
+
$this->assertDatabaseCount('users', 5);
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### PEST Expectations
|
|
1069
|
+
|
|
1070
|
+
```php
|
|
1071
|
+
expect($value)->toBe('expected');
|
|
1072
|
+
expect($value)->toBeTrue();
|
|
1073
|
+
expect($value)->toBeFalse();
|
|
1074
|
+
expect($value)->toBeNull();
|
|
1075
|
+
expect($value)->toBeEmpty();
|
|
1076
|
+
expect($value)->toHaveCount(5);
|
|
1077
|
+
expect($value)->toContain('item');
|
|
1078
|
+
expect($value)->toMatchArray(['key' => 'value']);
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
## Test Data Creation Strategy
|
|
1084
|
+
|
|
1085
|
+
### Golden Rule: Use API to Create Data
|
|
1086
|
+
|
|
1087
|
+
**Create test data through the API whenever possible** - this ensures data passes through the same validation and business logic as real users.
|
|
1088
|
+
|
|
1089
|
+
```php
|
|
1090
|
+
// ❌ BAD: Create directly with factory (bypasses validation)
|
|
1091
|
+
it('can update user', function () {
|
|
1092
|
+
$user = User::factory()->create([
|
|
1093
|
+
'email' => 'invalid', // Factory allows invalid data!
|
|
1094
|
+
]);
|
|
1095
|
+
// Test may pass but real API would reject this
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// ✅ GOOD: Create via API (same flow as real users)
|
|
1099
|
+
it('can update user', function () {
|
|
1100
|
+
// Create user through API
|
|
1101
|
+
$createResponse = $this->postJson('/api/users', validUserData());
|
|
1102
|
+
$createResponse->assertCreated();
|
|
1103
|
+
|
|
1104
|
+
$userId = $createResponse->json('data.id');
|
|
1105
|
+
|
|
1106
|
+
// Now test update
|
|
1107
|
+
$updateResponse = $this->putJson("/api/users/{$userId}", [
|
|
1108
|
+
'name_lastname' => 'Updated',
|
|
1109
|
+
]);
|
|
1110
|
+
|
|
1111
|
+
$updateResponse->assertOk();
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
### When to Use Factory vs API
|
|
1116
|
+
|
|
1117
|
+
| Scenario | Use Factory | Use API |
|
|
1118
|
+
| ------------------------------- | --------------------------- | ------------------------------- |
|
|
1119
|
+
| **Testing the endpoint itself** | ❌ | ✅ |
|
|
1120
|
+
| **Creating prerequisite data** | ⚠️ OK but be careful | ✅ Preferred |
|
|
1121
|
+
| **Testing relationships** | ✅ OK for related models | ✅ When testing the relation API |
|
|
1122
|
+
| **Performance (many records)** | ✅ Faster | ❌ Too slow |
|
|
1123
|
+
| **Testing edge cases** | ✅ Can create invalid states | ❌ API will reject |
|
|
1124
|
+
|
|
1125
|
+
### Recommended Pattern
|
|
1126
|
+
|
|
1127
|
+
```php
|
|
1128
|
+
describe('PUT /api/users/{id}', function () {
|
|
1129
|
+
|
|
1130
|
+
// Use API to create the user we'll update
|
|
1131
|
+
beforeEach(function () {
|
|
1132
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
1133
|
+
'email' => 'original@example.com',
|
|
1134
|
+
]));
|
|
1135
|
+
$response->assertCreated();
|
|
1136
|
+
|
|
1137
|
+
$this->testUser = $response->json('data');
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('updates user with valid data', function () {
|
|
1141
|
+
$response = $this->putJson("/api/users/{$this->testUser['id']}", [
|
|
1142
|
+
'name_lastname' => 'NewName',
|
|
1143
|
+
]);
|
|
1144
|
+
|
|
1145
|
+
$response->assertOk()
|
|
1146
|
+
->assertJsonPath('data.name_lastname', 'NewName');
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('fails with duplicate email', function () {
|
|
1150
|
+
// Create another user via API
|
|
1151
|
+
$this->postJson('/api/users', validUserData([
|
|
1152
|
+
'email' => 'other@example.com',
|
|
1153
|
+
]))->assertCreated();
|
|
1154
|
+
|
|
1155
|
+
// Try to update to existing email
|
|
1156
|
+
$response = $this->putJson("/api/users/{$this->testUser['id']}", [
|
|
1157
|
+
'email' => 'other@example.com',
|
|
1158
|
+
]);
|
|
1159
|
+
|
|
1160
|
+
$response->assertUnprocessable()
|
|
1161
|
+
->assertJsonValidationErrors(['email']);
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### Factory Usage Guidelines
|
|
1167
|
+
|
|
1168
|
+
**When Factory is OK:**
|
|
1169
|
+
|
|
1170
|
+
```php
|
|
1171
|
+
// ✅ OK: Creating many records for list/pagination tests
|
|
1172
|
+
it('returns paginated users', function () {
|
|
1173
|
+
User::factory()->count(25)->create(); // OK - testing pagination, not user creation
|
|
1174
|
+
|
|
1175
|
+
$response = $this->getJson('/api/users?per_page=10');
|
|
1176
|
+
|
|
1177
|
+
$response->assertOk()
|
|
1178
|
+
->assertJsonCount(10, 'data');
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// ✅ OK: Creating related data for relationship tests
|
|
1182
|
+
it('returns user with posts', function () {
|
|
1183
|
+
$user = User::factory()
|
|
1184
|
+
->has(Post::factory()->count(3))
|
|
1185
|
+
->create();
|
|
1186
|
+
|
|
1187
|
+
$response = $this->getJson("/api/users/{$user->id}?include=posts");
|
|
1188
|
+
|
|
1189
|
+
$response->assertOk()
|
|
1190
|
+
->assertJsonCount(3, 'data.posts');
|
|
1191
|
+
});
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
**When Factory is DANGEROUS:**
|
|
1195
|
+
|
|
1196
|
+
```php
|
|
1197
|
+
// ❌ DANGEROUS: Testing validation with factory-created data
|
|
1198
|
+
it('updates user', function () {
|
|
1199
|
+
// Factory may create data that doesn't match real validation rules!
|
|
1200
|
+
$user = User::factory()->create();
|
|
1201
|
+
|
|
1202
|
+
// This test doesn't prove the API works correctly
|
|
1203
|
+
});
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
## Debugging Test Failures
|
|
1209
|
+
|
|
1210
|
+
### Test Failed - Now What?
|
|
1211
|
+
|
|
1212
|
+
When a test fails, you MUST determine the root cause:
|
|
1213
|
+
|
|
1214
|
+
| Root Cause | Description | Action |
|
|
1215
|
+
| ----------------------------------- | --------------------------------- | ------------------------------------------- |
|
|
1216
|
+
| **Code Bug** | Actual application code is broken | Fix the application code |
|
|
1217
|
+
| **Test Bug** | Test code is incorrect | Fix the test |
|
|
1218
|
+
| **Business Logic Misunderstanding** | Test doesn't match requirements | Clarify requirements, then fix test or code |
|
|
1219
|
+
| **Environment Issue** | Database, config, timing issue | Fix environment/setup |
|
|
1220
|
+
|
|
1221
|
+
### Debugging Checklist
|
|
1222
|
+
|
|
1223
|
+
```mermaid
|
|
1224
|
+
flowchart TD
|
|
1225
|
+
A[TEST FAILED] --> B[1. Read error message carefully]
|
|
1226
|
+
B --> C[2. Check test code]
|
|
1227
|
+
C --> D{Is test code correct?}
|
|
1228
|
+
|
|
1229
|
+
D -->|NO| E[Fix test code]
|
|
1230
|
+
D -->|YES| F[Check actual code]
|
|
1231
|
+
|
|
1232
|
+
F --> G{Is it a business logic issue?}
|
|
1233
|
+
|
|
1234
|
+
G -->|YES| H[Clarify requirements]
|
|
1235
|
+
G -->|NO| I[Fix code bug]
|
|
1236
|
+
|
|
1237
|
+
C -.-> C1[Correct endpoint?]
|
|
1238
|
+
C -.-> C2[Correct data?]
|
|
1239
|
+
C -.-> C3[Correct assertion?]
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### Debugging Techniques
|
|
1243
|
+
|
|
1244
|
+
#### 1. Print Response Data
|
|
1245
|
+
|
|
1246
|
+
```php
|
|
1247
|
+
it('creates user', function () {
|
|
1248
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
1249
|
+
|
|
1250
|
+
// Debug: Print full response
|
|
1251
|
+
dump($response->json());
|
|
1252
|
+
dump($response->status());
|
|
1253
|
+
|
|
1254
|
+
$response->assertCreated();
|
|
1255
|
+
});
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
#### 2. Check Database State
|
|
1259
|
+
|
|
1260
|
+
```php
|
|
1261
|
+
it('creates user', function () {
|
|
1262
|
+
$response = $this->postJson('/api/users', validUserData([
|
|
1263
|
+
'email' => 'test@example.com',
|
|
1264
|
+
]));
|
|
1265
|
+
|
|
1266
|
+
// Debug: Check what's actually in database
|
|
1267
|
+
dump(User::where('email', 'test@example.com')->first());
|
|
1268
|
+
dump(User::count());
|
|
1269
|
+
|
|
1270
|
+
$response->assertCreated();
|
|
1271
|
+
});
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
#### 3. Check Validation Errors
|
|
1275
|
+
|
|
1276
|
+
```php
|
|
1277
|
+
it('creates user', function () {
|
|
1278
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
1279
|
+
|
|
1280
|
+
// If 422, check which fields failed
|
|
1281
|
+
if ($response->status() === 422) {
|
|
1282
|
+
dump($response->json('errors'));
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
$response->assertCreated();
|
|
1286
|
+
});
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
#### 4. Isolate the Problem
|
|
1290
|
+
|
|
1291
|
+
```php
|
|
1292
|
+
// Break down complex test into smaller parts
|
|
1293
|
+
it('debug: check data is valid', function () {
|
|
1294
|
+
$data = validUserData();
|
|
1295
|
+
dump($data);
|
|
1296
|
+
|
|
1297
|
+
// Check each field manually
|
|
1298
|
+
expect($data['email'])->toContain('@');
|
|
1299
|
+
expect(strlen($data['password']))->toBeGreaterThanOrEqual(8);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('debug: check API accepts data', function () {
|
|
1303
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
1304
|
+
dump($response->status());
|
|
1305
|
+
dump($response->json());
|
|
1306
|
+
});
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
### Common Failure Patterns
|
|
1310
|
+
|
|
1311
|
+
#### Pattern 1: Test Passes Locally, Fails in CI
|
|
1312
|
+
|
|
1313
|
+
```php
|
|
1314
|
+
// ❌ Problem: Depends on database state
|
|
1315
|
+
it('gets user', function () {
|
|
1316
|
+
$response = $this->getJson('/api/users/1'); // ID 1 may not exist!
|
|
1317
|
+
$response->assertOk();
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// ✅ Solution: Create own test data
|
|
1321
|
+
it('gets user', function () {
|
|
1322
|
+
$user = User::factory()->create();
|
|
1323
|
+
$response = $this->getJson("/api/users/{$user->id}");
|
|
1324
|
+
$response->assertOk();
|
|
1325
|
+
});
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
#### Pattern 2: Test Depends on Order
|
|
1329
|
+
|
|
1330
|
+
```php
|
|
1331
|
+
// ❌ Problem: Tests affect each other
|
|
1332
|
+
it('creates user', function () {
|
|
1333
|
+
$this->postJson('/api/users', ['email' => 'test@example.com']);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('fails with duplicate email', function () {
|
|
1337
|
+
// This only works if previous test ran first!
|
|
1338
|
+
$this->postJson('/api/users', ['email' => 'test@example.com'])
|
|
1339
|
+
->assertUnprocessable();
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// ✅ Solution: Each test is independent
|
|
1343
|
+
it('fails with duplicate email', function () {
|
|
1344
|
+
// Create first user in THIS test
|
|
1345
|
+
$this->postJson('/api/users', validUserData(['email' => 'test@example.com']))
|
|
1346
|
+
->assertCreated();
|
|
1347
|
+
|
|
1348
|
+
// Now test duplicate
|
|
1349
|
+
$this->postJson('/api/users', validUserData(['email' => 'test@example.com']))
|
|
1350
|
+
->assertUnprocessable();
|
|
1351
|
+
});
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
#### Pattern 3: Wrong Assertion
|
|
1355
|
+
|
|
1356
|
+
```php
|
|
1357
|
+
// ❌ Problem: Asserting wrong thing
|
|
1358
|
+
it('creates user', function () {
|
|
1359
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
1360
|
+
|
|
1361
|
+
// Wrong: assertOk is 200, but POST returns 201
|
|
1362
|
+
$response->assertOk();
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// ✅ Solution: Use correct assertion
|
|
1366
|
+
it('creates user', function () {
|
|
1367
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
1368
|
+
|
|
1369
|
+
// Correct: POST returns 201 Created
|
|
1370
|
+
$response->assertCreated();
|
|
1371
|
+
});
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
#### Pattern 4: Test Data Doesn't Match Validation
|
|
1375
|
+
|
|
1376
|
+
```php
|
|
1377
|
+
// ❌ Problem: Test data is invalid
|
|
1378
|
+
function validUserData(array $overrides = []): array {
|
|
1379
|
+
return array_merge([
|
|
1380
|
+
'name_kana_lastname' => 'たなか', // Wrong! Must be Katakana
|
|
1381
|
+
], $overrides);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// ✅ Solution: Match validation rules exactly
|
|
1385
|
+
function validUserData(array $overrides = []): array {
|
|
1386
|
+
return array_merge([
|
|
1387
|
+
'name_kana_lastname' => 'タナカ', // Correct: Katakana
|
|
1388
|
+
], $overrides);
|
|
1389
|
+
}
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
### When Test Fails, Ask These Questions
|
|
1393
|
+
|
|
1394
|
+
1. **Is the test testing the right thing?**
|
|
1395
|
+
- Does the endpoint exist?
|
|
1396
|
+
- Is the HTTP method correct?
|
|
1397
|
+
- Are we asserting the right status code?
|
|
1398
|
+
|
|
1399
|
+
2. **Is the test data correct?**
|
|
1400
|
+
- Does it match validation rules?
|
|
1401
|
+
- Are required fields present?
|
|
1402
|
+
- Are formats correct (email, date, etc.)?
|
|
1403
|
+
|
|
1404
|
+
3. **Is the test isolated?**
|
|
1405
|
+
- Does it depend on other tests?
|
|
1406
|
+
- Does it depend on existing database data?
|
|
1407
|
+
- Does it clean up after itself?
|
|
1408
|
+
|
|
1409
|
+
4. **Is this actually a bug?**
|
|
1410
|
+
- Check the business requirements
|
|
1411
|
+
- Maybe the code is correct and test is wrong
|
|
1412
|
+
- Maybe requirements changed
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
## Best Practices
|
|
1417
|
+
|
|
1418
|
+
### DO
|
|
1419
|
+
|
|
1420
|
+
```php
|
|
1421
|
+
// ✅ Use describe() to group related tests
|
|
1422
|
+
describe('POST /api/users', function () {
|
|
1423
|
+
it('正常: creates user with valid data', function () { ... });
|
|
1424
|
+
it('異常: fails with invalid email', function () { ... });
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// ✅ Use helper functions for test data
|
|
1428
|
+
function validUserData(array $overrides = []): array {
|
|
1429
|
+
return array_merge([...], $overrides);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// ✅ Use API to create test data when testing mutations
|
|
1433
|
+
it('正常: updates user', function () {
|
|
1434
|
+
$createResponse = $this->postJson('/api/users', validUserData());
|
|
1435
|
+
$userId = $createResponse->json('data.id');
|
|
1436
|
+
|
|
1437
|
+
$this->putJson("/api/users/{$userId}", ['name' => 'New'])
|
|
1438
|
+
->assertOk();
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// ✅ Use PEST expectations for cleaner assertions
|
|
1442
|
+
expect($response->json('data.email'))->toBe('test@example.com');
|
|
1443
|
+
|
|
1444
|
+
// ✅ Check database state after mutations
|
|
1445
|
+
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
### DON'T
|
|
1449
|
+
|
|
1450
|
+
```php
|
|
1451
|
+
// ❌ Hardcode IDs
|
|
1452
|
+
$this->getJson('/api/users/1');
|
|
1453
|
+
|
|
1454
|
+
// ❌ Skip abnormal cases (異常系)
|
|
1455
|
+
// Only testing happy path (正常系) is NOT enough!
|
|
1456
|
+
|
|
1457
|
+
// ❌ Test multiple things in one test
|
|
1458
|
+
it('does everything', function () { ... }); // Too broad
|
|
1459
|
+
|
|
1460
|
+
// ❌ Depend on database state from other tests
|
|
1461
|
+
// Tests should be isolated
|
|
1462
|
+
|
|
1463
|
+
// ❌ Assume test failure = code bug
|
|
1464
|
+
// Always analyze: Is it test bug? Code bug? Business logic misunderstanding?
|
|
1465
|
+
|
|
1466
|
+
// ❌ Create test data that bypasses validation
|
|
1467
|
+
User::factory()->create(['email' => 'invalid']); // API would reject this!
|
|
1468
|
+
|
|
1469
|
+
// ❌ Test without understanding requirements
|
|
1470
|
+
// Read the spec first, then write tests
|
|
1471
|
+
|
|
1472
|
+
// ❌ Copy-paste tests without understanding
|
|
1473
|
+
// Each test should test ONE specific scenario
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
---
|
|
1477
|
+
|
|
1478
|
+
## Test Failure Analysis Checklist
|
|
1479
|
+
|
|
1480
|
+
Before fixing a failed test, answer these questions:
|
|
1481
|
+
|
|
1482
|
+
| Question | If Yes |
|
|
1483
|
+
| ----------------------------------------------------- | ------------------------------------- |
|
|
1484
|
+
| Is the test code correct? (endpoint, data, assertion) | Check application code |
|
|
1485
|
+
| Is the test data valid according to business rules? | Fix test data |
|
|
1486
|
+
| Does the test depend on other tests or data? | Make test independent |
|
|
1487
|
+
| Is the assertion correct for this endpoint? | Fix assertion (e.g., 201 not 200) |
|
|
1488
|
+
| Did the requirements change? | Update test to match new requirements |
|
|
1489
|
+
| Is this actually expected behavior? | Test is correct, code is a bug |
|
|
1490
|
+
|
|
1491
|
+
**Remember**: A failing test is information. Analyze it carefully before "fixing" anything.
|
|
1492
|
+
|
|
1493
|
+
---
|
|
1494
|
+
|
|
1495
|
+
## Summary
|
|
1496
|
+
|
|
1497
|
+
| Test Type | What to Test | When |
|
|
1498
|
+
| --------------------- | ------------------------------ | ---------------- |
|
|
1499
|
+
| **Normal (正常系)** | Happy path, expected behavior | Always |
|
|
1500
|
+
| **Abnormal (異常系)** | Errors, validation, edge cases | Always |
|
|
1501
|
+
| **Unit** | Pure logic, no HTTP | Complex services |
|
|
1502
|
+
| **Feature** | Full HTTP cycle | Every endpoint |
|
|
1503
|
+
|
|
1504
|
+
**Rule**: No endpoint is complete without both normal AND abnormal test cases.
|