Bài 31 — Kiểm thử database, auth, policy và queue: Một response 200 không chứng minh dữ liệu đúng; một response 403 không chứng minh request không gây tác dụng phụ. Trong TaskFlow, mỗi test cần mô tả ranh giới đang kiểm tra và trạng thái nào phải còn đúng sau khi request kết thúc.
1. Viết ma trận hành vi trước assertion
| Tình huống | Kết quả HTTP | Trạng thái cần kiểm tra |
|---|---|---|
| Guest gọi API tạo task | 401 | Không insert task |
| Owner gửi title trống | 422 | Không insert task |
| User khác hoàn tất task | 403 | Status không đổi |
| Owner dùng task dưới sai project | 404 | Status không đổi |
| Hoàn tất task đã done | 409 | Không tạo lần chuyển trạng thái mới |
| Transaction tạo task rollback | Không áp dụng | Không có task và job mới |
Mã HTTP phụ thuộc route và định dạng request: guest truy cập dashboard HTML được redirect login, còn getJson nhận 401. Không viết một assertion chung cho mọi ngữ cảnh. Đặt tên theo hành vi người dùng quan sát được để refactor controller không làm mất ý nghĩa test.
2. Cô lập database trước khi chạy migration
Các feature test hiện tại ép SQLite trong bộ nhớ trong setUp sau parent::setUp. Đặt url=null tránh DB_URL ngoài môi trường ghi đè tên database; purge bỏ connection đã resolve với cấu hình cũ. Import facade DB và gọi các lệnh sau trong test, không trong request production.
config([
'database.default' => 'sqlite',
'database.connections.sqlite.database' => ':memory:',
'database.connections.sqlite.url' => null,
]);
DB::purge('sqlite');
$this->artisan('migrate', ['--force' => true])->assertSuccessful();Test dùng factory tạo dữ liệu nhỏ, không phụ thuộc seeder demo hoặc tài khoản đã có trên máy. Sau thao tác, đọc fresh model để tránh assertion trên object cũ. Với đường từ chối, kiểm tra count/status/path thay vì chỉ nhìn status code. Không dùng database thật của khách hàng; migrate --force không tự bảo vệ khỏi nhầm connection.
RefreshDatabase thuận tiện cho nhiều test CRUD nhưng có thể bọc test trong transaction. Những test after-commit của TaskFlow chủ động không dùng trait đó để quan sát commit thực. SQLite giúp feedback nhanh nhưng không chứng minh locking, collation, JSON/index hay deadlock của MySQL/PostgreSQL. Muốn xác minh hành vi phụ thuộc engine cần thêm suite dùng đúng engine trong môi trường riêng.
3. Authentication khác authorization
SessionAuthenticationTest gửi email/password tới /login, kiểm tra danh tính, session ID thay đổi và logout xóa dữ liệu session/token cũ. Nó còn kiểm tra mật khẩu sai không được flash và IP rate limit trả 429. Trong khi đó actingAs ở ProjectAuthorizationTest bỏ qua quy trình login để tập trung vào quyền tài nguyên. Cả hai loại cần tồn tại, không thay thế nhau.
SanctumTokenTest dùng token được tạo trong database thử nghiệm để kiểm tra ability đọc/ghi, owner, hết hạn và revoke. Không log plain token. Token có tasks:write vẫn không được sửa project của người khác. CSRF cho session route là lớp riêng: feature test Laravel thường bỏ qua CSRF; SessionCsrfTest bật lại middleware để kiểm tra nhánh thực tế. Laravel 13 có kiểm tra same-origin qua Fetch Metadata, nên không khẳng định mọi POST thiếu token đều 419.
4. Kiểm tra nested binding dù người dùng là owner
Đoạn dưới trích ProjectAuthorizationTest. Cả hai project thuộc cùng owner, nhưng task của project thứ hai không hợp lệ trong URL project thứ nhất. Test phải chặn lỗi ghép resource, không chỉ lỗi thiếu quyền.
$first = Project::factory()->create();
$second = Project::factory()->for($first->owner, 'owner')->create();
$task = Task::factory()->for($second)->create();
$this->actingAs($first->owner)
->patchJson(route('project-tasks.complete', [$first, $task]))
->assertNotFound();
$this->assertSame('todo', $task->fresh()->status);Import Project, Task từ App\Models. Test khác gửi owner_id, assignee_id và status giả mạo khi tạo để xác nhận server không dùng field ngoài whitelist. Một policy pass không thay cho scoped binding; validation pass không thay cho authorization. Tránh withoutMiddleware toàn cục vì nó có thể bỏ chính hàng rào muốn đo.
5. Fake queue và chạy worker chứng minh hai điều khác nhau
Queue::fake phù hợp hỏi “đúng job đã được yêu cầu chưa?”. Nó không gọi handle, không thử serialize job vào backend và không chứng minh worker đang nghe đúng queue. TaskQueueTest đi thêm một bước: database queue thật và queue:work --once trong cùng process test. Đoạn trích dưới cần các import CreateTask, CreateTaskData, Project và facade Cache/DB; setup database như phần 2 và BROADCAST_CONNECTION=null từ phpunit.xml.
// TaskQueueTest: database queue on the same isolated SQLite connection.
config(['queue.default' => 'database',
'queue.connections.database.connection' => 'sqlite', 'cache.default' => 'array']);
$project = Project::factory()->create();
$key = 'taskflow:project:'.$project->id.':task-count:v1';
Cache::put($key, 0, 30);
DB::transaction(function () use ($project) {
app(CreateTask::class)->handle($project, new CreateTaskData('Queued task'));
$this->assertDatabaseCount('jobs', 0);
});
$this->assertDatabaseCount('jobs', 1);
$this->assertSame(0, Cache::get($key));
$this->artisan('queue:work', ['connection' => 'database', '--queue' => 'maintenance',
'--once' => true, '--tries' => 3, '--timeout' => 15])->assertSuccessful();
$this->assertDatabaseCount('jobs', 0);
$this->assertNull(Cache::get($key));
$this->assertDatabaseCount('failed_jobs', 0);Trước commit chưa có job; sau commit có đúng một job maintenance; worker lấy/xóa job, xóa cache và không tạo failed job. Test rollback riêng xác nhận không còn task/job. Test gọi handle hai lần kiểm tra thao tác xóa key là idempotent và không xóa key project khác. Đây không phải bằng chứng exactly-once, timeout cưỡng chế trên Windows, worker nhiều process hoặc Redis hoạt động.
Array cache trong test được chia sẻ vì worker chạy cùng process. Production nhiều process cần cache chung nếu muốn worker invalidation tác động tới web. Không đổi sang sync queue rồi tuyên bố đã kiểm chứng database queue. Không fake toàn bộ events khi cần quan sát listener TaskCreated thực sự dispatch job.
6. Regression cho file đính kèm
Bài này bổ sung ba test vào TaskAttachmentTest: file PDF 2.049 KB phải bị từ chối, attachment dưới sai task phải 404 dù cùng owner, và metadata còn nhưng file thiếu phải 404 mà không tự xóa metadata. Ví dụ oversized test kiểm tra cả database, storage và notification:
// TaskAttachmentTest setup already uses isolated DB, Storage::fake('local')
// and Notification::fake(). Imports: Task model, UploadedFile and Storage facade.
$task = Task::factory()->create();
$this->actingAs($task->project->owner)
->postJson(route('attachments.store', [$task->project, $task]), [
'attachment' => UploadedFile::fake()->create('large.pdf', 2049, 'application/pdf'),
])->assertUnprocessable()->assertJsonValidationErrors('attachment');
$this->assertDatabaseCount('task_attachments', 0);
$this->assertSame([], Storage::disk('local')->allFiles());
Notification::assertNothingSent();Fake file mô phỏng dung lượng/MIME, không chứng minh PDF hợp lệ hoặc không có malware. Storage::fake không kiểm tra quyền filesystem hay S3. Notification::fake xác nhận ý định gửi, không chứng minh thư đến inbox. Những giới hạn đó quyết định cần kiểm thử tích hợp nào tiếp theo, không làm mất giá trị regression nhanh.
7. Chạy lại có chủ đích
php artisan test --filter=SessionAuthenticationTest
php artisan test --filter=ProjectAuthorizationTest
php artisan test --filter=SanctumTokenTest
php artisan test --filter=TaskQueueTest
php artisan test --filter=TaskAttachmentTest
php artisan test
Kết quả checkpoint: 97 tests, 435 assertions đều pass trên PHP 8.4.25 và Pest 4.7.8. Test mới không thay code production; không chạy migration trên database phát triển. Số assertion không đo coverage hay chứng nhận an toàn. Khi test fail, đọc assertion đầu tiên, xem fixture và cấu hình trước khi tăng timeout hoặc thêm sleep.
Bài tập: thêm case title hợp lệ nhưng project không tồn tại, token read-only cố ghi và job gặp dependency lỗi. Với mỗi case, ghi rõ test dùng fake hay thật, side effect phải không xảy ra và điều gì vẫn chưa được chứng minh. Bài kế tiếp đưa những kiểm tra này vào CI, không biến một lần pass local thành tuyên bố pipeline đã chạy.
Tham khảo: Laravel Database Testing, Laravel Mocking. Điều hướng: Bài 30: Pest · Lộ trình.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.