From 0dce2ef8128416ba6d68f148045f7e5e9262e2b3 Mon Sep 17 00:00:00 2001 From: thienpt Date: Tue, 8 Sep 2026 15:01:01 +0700 Subject: [PATCH] feat: enforce exhaustive unit testing and 100% SonarQube branch coverage - Enforce mandatory handle() and failed() method testing for all Jobs and Commands - Ban shallow reflection-only helper testing in Jobs - Mandate actual SQLite in-memory query execution for Repositories (ban Builder::class assertions alone) - Enforce full branch coverage for SonarQube (ternary, null-coalescing, catch blocks, match) - Update JobTest templates for PHPUnit and Pest to include handle() and failed() methods - Update verification checklist and master system prompt with strict branch gatekeepers Co-Authored-By: Claude Fable 5 --- .claude/skills/laravel-unit-test/skill.md | 19 ++++++++++++++----- .../components/04-repositories-and-queries.md | 17 +++++++++++++++++ docs/components/06-jobs-and-queues.md | 13 +++++++++++++ rules/system-prompt-unit-test.md | 18 +++++++++++++----- rules/verification-checklist.md | 19 +++++++++++++------ templates/pest/JobTest.template.php | 12 ++++++++++++ templates/phpunit/JobTest.template.php | 14 ++++++++++++++ 7 files changed, 96 insertions(+), 16 deletions(-) diff --git a/.claude/skills/laravel-unit-test/skill.md b/.claude/skills/laravel-unit-test/skill.md index 7dd0ab0..653ca4d 100644 --- a/.claude/skills/laravel-unit-test/skill.md +++ b/.claude/skills/laravel-unit-test/skill.md @@ -83,10 +83,18 @@ Người dùng có thể kích hoạt linh hoạt theo các chế độ (từ qu - **Dùng Laravel Fakes**: `Http::fake()`, `Queue::fake()`, `Event::fake()`, `Storage::fake()`. - **Dùng Object thật (Không Mock)**: DTOs, Value Objects, Collections, Request payload dạng mảng. -### Bước 4: Lập Ma Trận Test Scenarios (Tối thiểu cho mỗi public method) -- **1 Happy Path**: Đầu vào hợp lệ -> Thực thi thành công -> Trả về đúng kết quả. -- **N Edge Cases**: Input biên (0, số âm, rỗng `""`, `[]`, `null`, unicode). -- **N Exception Cases**: Tham số sai ném `InvalidArgumentException`, service ngoài lỗi ném `PaymentFailedException`,... +### Bước 4: Lập Ma Trận Test Scenarios (Bắt Buộc Đầy Đủ & Không Bỏ Sót) +- **Bắt buộc 100% Public Methods**: Mọi phương thức public đều phải có test. + - *Nếu là Job / Command*: **BẮT BUỘC 100% phải test phương thức `handle()`** và `failed()`. Tuyệt đối CẤM chỉ test hàm helper phụ (`buildRedisKey`) bằng reflection! + - *Nếu là Repository*: **BẮT BUỘC phải test kết quả truy vấn / cập nhật dữ liệu thật** trên SQLite in-memory, tuyệt đối CẤM chỉ assert `Builder::class`! +- **Bao phủ 100% nhánh rẽ SonarQube**: + - Mọi toán tử `??` (Null Coalescing) và `?:` (Ternary) phải có test case cho cả 2 vế. + - Mọi khối `try - catch - finally` bắt buộc phải có test case ép mock ném Exception để chạy qua khối `catch`. + - Mọi `if`, `else if`, `else`, `switch`, `match` (kể cả nhánh `default`) đều phải được kích hoạt. +- **Tối thiểu cho mỗi public method**: + - **1 Happy Path**: Đầu vào hợp lệ -> Thực thi thành công -> Trả về đúng kết quả. + - **N Edge Cases**: Input biên (0, số âm, rỗng `""`, `[]`, `null`, unicode). + - **N Exception Cases**: Tham số sai ném `InvalidArgumentException`, service ngoài lỗi ném `PaymentFailedException`,... ### Bước 5: Sinh Mã Nguồn Test 1. Xác định đường dẫn file test theo quy tắc Mirror: @@ -95,8 +103,9 @@ Người dùng có thể kích hoạt linh hoạt theo các chế độ (từ qu 2. Tham chiếu hướng dẫn tương ứng trong `docs/components/` và template trong `templates/`. 3. Viết đầy đủ code: - Tuân thủ mô hình **AAA** (`// Arrange`, `// Act`, `// Assert`). - - Mock expectation có số lần gọi rõ ràng: `->once()`, `->never()`. + - Mock expectation có số lần gọi nghiêm ngặt: `->once()`, `->never()`. - Dùng Strict assertion: `$this->assertSame()` hoặc `expect(...)->toBe()`. + - Đảm bảo quét sạch tất cả các nhánh điều kiện để SonarQube Line & Branch Coverage đạt > 85% - 95%. ### Bước 6: Thẩm định & Lưu File 1. Tự kiểm tra file test qua `rules/verification-checklist.md`. diff --git a/docs/components/04-repositories-and-queries.md b/docs/components/04-repositories-and-queries.md index 4e942fb..167ca22 100644 --- a/docs/components/04-repositories-and-queries.md +++ b/docs/components/04-repositories-and-queries.md @@ -98,3 +98,20 @@ class UserRepositoryTest extends TestCase } } ``` + +--- + +## 3. Bẫy Nghiêm Trọng Cần Tránh Khi Test Repository + +### ❌ Bẫy Anti-Pattern: Chỉ kiểm tra `assertInstanceOf(Builder::class)` +Nhiều bài test chỉ viết: +```php +$query = $repo->search(['name' => 'ABC']); +$this->assertInstanceOf(Builder::class, $query); // ❌ SAI LẦM: Chỉ chạm đúng 1-2 dòng return! +``` +- **Hậu quả**: Toàn bộ logic bên trong các câu `where()`, `join()`, `groupBy()`, `orderBy()`, xử lý lọc ngày tháng, lọc trạng thái... **hoàn toàn không được thực thi**, dẫn đến độ phủ SonarQube của Repository chỉ đạt 10% - 25%! +- **Quy tắc vàng**: + 1. Kết hợp trait `use RefreshDatabase;` với SQLite in-memory siêu tốc. + 2. Tạo bản ghi mẫu bằng `Model::factory()->create(...)` thỏa mãn và không thỏa mãn điều kiện lọc. + 3. Thực thi query lấy kết quả thật (`$results = $query->get()` hoặc gọi thẳng method `$repo->findActive(...)`) và assert chính xác số lượng bản ghi (`assertCount`) và ID của các bản ghi thỏa mãn. + 4. Mọi phương thức update, delete, bulkInsert của Repository đều phải được gọi và assert trạng thái database bằng `$this->assertDatabaseHas(...)` hoặc `$this->assertDatabaseMissing(...)`. diff --git a/docs/components/06-jobs-and-queues.md b/docs/components/06-jobs-and-queues.md index 8bdef01..fe94a5b 100644 --- a/docs/components/06-jobs-and-queues.md +++ b/docs/components/06-jobs-and-queues.md @@ -109,3 +109,16 @@ class SendWelcomeEmailJobTest extends TestCase } } ``` + +--- + +## 3. Bẫy Nghiêm Trọng Cần Tránh Khi Test Jobs + +### ❌ Bẫy Anti-Pattern: Chỉ dùng Reflection test hàm helper nhỏ +Nhiều AI và lập trình viên hay dùng `ReflectionMethod` để test các hàm helper nhỏ như `buildRedisKey()`, `getFilePath()` và bỏ qua hoàn toàn phương thức chính **`handle()`**. +- **Hậu quả**: SonarQube báo độ phủ của Job chỉ đạt 1% - 10%, vì 90% dòng code nằm trong `handle()` không hề được chạy! +- **Quy tắc vàng**: + 1. **BẮT BUỘC 100% phải test method `handle()`**. + 2. Mock toàn bộ dependencies của `handle()`: Storage (`Storage::fake()`), Database/Repository, Redis (`Redis::shouldReceive()`), Mailer. + 3. Phải có bài test cho các trường hợp rẽ nhánh trong `handle()`: khi dữ liệu rỗng, khi export thành công, khi import lỗi từng dòng. + 4. Phải có bài test gọi method `failed(?Throwable $e)` để kiểm tra việc ghi log hoặc bắn alert khi job thất bại. diff --git a/rules/system-prompt-unit-test.md b/rules/system-prompt-unit-test.md index 877c2bf..6aa9112 100644 --- a/rules/system-prompt-unit-test.md +++ b/rules/system-prompt-unit-test.md @@ -8,11 +8,19 @@ Bạn là một Chuyên gia Kỹ thuật Cấp cao về Kiểm thử Phần mề 1. **Tính Cô Lập Tuyệt Đối (Zero Leakage)**: Mọi Unit Test bạn tạo ra không được phụ thuộc vào Database ngoại vi hoặc Network thật. Mọi external dependencies đều phải được Mock bằng Mockery hoặc Laravel Built-in Fakes. 2. **Tuân Thủ Mô Hình AAA**: Bắt buộc chia rõ 3 khối: `// Arrange`, `// Act`, `// Assert`. -3. **Độ Phủ Biên Toàn Diện (Exhaustive Coverage)**: Không bao giờ chỉ viết Happy Path. Bắt buộc quét sạch: - - Happy Path (Đầu vào chuẩn). - - Negative Path (Đầu vào không hợp lệ / Validation lỗi). - - Boundary Cases (Min, Max, Zero, Null, Empty Array). - - Exception Cases (Lỗi mạng, External Service ném Exception, Unauthorized). +3. **Độ Phủ Biên & Nhánh Toàn Diện (Exhaustive & SonarQube Branch Coverage)**: + - **Bắt buộc 100% Public Methods**: Mọi phương thức public của class đều phải có bài test. Tuyệt đối không được bỏ qua bất kỳ public method nào. + - **Đặc thù với Jobs & Console Commands**: BẮT BUỘC 100% phải test phương thức **`handle()`** (với đầy đủ luồng thực thi, mock S3, Redis, Database, Mail) và phương thức `failed()`. Tuyệt đối CẤM chỉ test hàm helper phụ (như `buildRedisKey`) mà bỏ qua `handle()`. + - **Đặc thù với Repositories**: BẮT BUỘC phải test kết quả trả về của các phương thức truy vấn và cập nhật dữ liệu thật (trên SQLite in-memory). Tuyệt đối CẤM chỉ assert lỏng lẻo kiểu `$this->assertInstanceOf(Builder::class)`. + - **Bao phủ 100% mọi nhánh rẽ SonarQube**: + - Mọi toán tử `??` (Null Coalescing) và `?:` (Ternary) phải có test case cho cả 2 vế. + - Mọi khối `try - catch - finally` bắt buộc phải có test case ép mock ném Exception để chạy qua khối `catch`. + - Mọi `if`, `else if`, `else`, `switch`, `match` (kể cả nhánh `default`) đều phải được kích hoạt. + - Không bao giờ chỉ viết Happy Path. Bắt buộc quét sạch: + - Happy Path (Đầu vào chuẩn). + - Negative Path (Đầu vào không hợp lệ / Validation lỗi). + - Boundary Cases (Min, Max, Zero, Null, Empty Array). + - Exception Cases (Lỗi mạng, External Service ném Exception, Unauthorized). 4. **Tự Động Thích Ứng Framework & Phiên Bản (Bao Quát PHP 8.4+ & Laravel 12/13)**: - Nếu code dùng tính năng **PHP 8.4+** (Property Hooks, Asymmetric Visibility): Test trực tiếp hook logic và xác thực tính toàn vẹn của state. - Nếu dự án dùng **Pest PHP (v2/v3)**: Sinh test chuẩn Functional syntax (`it()`, `describe()`, `expect()`) và hỗ trợ Architectural Testing (`arch()`). diff --git a/rules/verification-checklist.md b/rules/verification-checklist.md index a552287..0960d0a 100644 --- a/rules/verification-checklist.md +++ b/rules/verification-checklist.md @@ -19,23 +19,30 @@ Trước khi xuất bất kỳ file test nào cho người dùng, Agent **BẮT ### [ ] 4. Không Over-Mock DTO/Value Object (Are value objects kept real?) - Các đối tượng DTO, Value Object, Collection có được tạo bằng instance thật thay vì mock không? -### [ ] 5. Độ Phủ Ngoại Lệ & Kịch Bản Biên (Are edge cases & exceptions covered?) +### [ ] 5. Độ Phủ Ngoại Lệ, Kịch Bản Biên & SonarQube Branches (Are edge cases & branches covered?) - Đã có test case cho trường hợp ném ngoại lệ (Exception) chưa? - Đã kiểm tra các trường hợp biên: `null`, mảng rỗng `[]`, số `0`, số âm chưa? +- Mọi toán tử `??` và `?:` đã có test case cho cả 2 vế chưa? +- Mọi khối `try/catch` đã có test case ép mock ném Exception để chạy qua khối `catch` chưa? -### [ ] 6. Assertions Chặt Chẽ & Có Nghĩa (Are assertions meaningful?) +### [ ] 6. Không Bỏ Sót Phương Thức Cốt Lõi (Are all core methods tested?) +- Nếu là **Job hoặc Console Command**: Đã test phương thức **`handle()`** chưa? Tuyệt đối không được chỉ test method helper phụ (như `buildRedisKey`) bằng reflection! +- Nếu là **Repository**: Đã test các phương thức truy vấn và cập nhật dữ liệu với kết quả trả về cụ thể (trên SQLite in-memory) chưa? Tuyệt đối không chỉ assert `Builder::class`! +- Tất cả các `public methods` của class đã có ít nhất một bài test tương ứng chưa? + +### [ ] 7. Assertions Chặt Chẽ & Có Nghĩa (Are assertions meaningful?) - Có sử dụng Strict Assertion (`assertSame`, `toBe`) thay vì loose assertion không? - Có bị dính assertion rỗng kiểu `$this->assertTrue(true)` không? -### [ ] 7. Đặt Tên Chuẩn & Dễ Đọc (Are method names expressive?) +### [ ] 8. Đặt Tên Chuẩn & Dễ Đọc (Are method names expressive?) - Tên test method có thể hiện rõ "Hành động + Kết quả + Ngữ cảnh" không? (Ví dụ: `it_throws_exception_when_balance_is_insufficient`) -### [ ] 8. Cú Pháp Đúng Phiên Bản Framework (Is syntax version-accurate?) +### [ ] 9. Cú Pháp Đúng Phiên Bản Framework (Is syntax version-accurate?) - Nếu là PHPUnit 10+: Đã dùng Attribute `#[Test]` và `#[DataProvider]` thay cho DocBlock cũ chưa? - Nếu là Pest: Đã dùng đúng cú pháp `describe()`, `it()`, `expect()` chưa? -### [ ] 9. Import Đầy Đủ & Namespace Chính Xác (Are imports complete?) +### [ ] 10. Import Đầy Đủ & Namespace Chính Xác (Are imports complete?) - Tất cả các class, exception, interface, attribute sử dụng trong file test đã có lệnh `use ...` ở đầu file chưa? (Tránh lỗi `Class not found`). -### [ ] 10. Không Gây Rò Rỉ Trạng Thái (Zero State Leakage) +### [ ] 11. Không Gây Rò Rỉ Trạng Thái (Zero State Leakage) - Nếu có dùng `Carbon::setTestNow()` hay mock Facade tĩnh, đã đảm bảo dọn dẹp hoặc reset về trạng thái ban đầu chưa? diff --git a/templates/pest/JobTest.template.php b/templates/pest/JobTest.template.php index 77fcbf6..ec6c56e 100644 --- a/templates/pest/JobTest.template.php +++ b/templates/pest/JobTest.template.php @@ -16,4 +16,16 @@ describe('{{ClassName}}', function () { // Assert expect(true)->toBeTrue(); }); + + it('handles job failure cleanly', function () { + // Arrange + // $job = new {{ClassName}}(...); + // $exception = new \RuntimeException('Job failed'); + + // Act + // $job->failed($exception); + + // Assert + expect(true)->toBeTrue(); + }); }); diff --git a/templates/phpunit/JobTest.template.php b/templates/phpunit/JobTest.template.php index b02266d..aa1cf4a 100644 --- a/templates/phpunit/JobTest.template.php +++ b/templates/phpunit/JobTest.template.php @@ -32,4 +32,18 @@ class {{ClassName}}Test extends TestCase // Assert $this->assertTrue(true); } + + #[Test] + public function it_handles_job_failure_cleanly(): void + { + // Arrange + // $job = new {{ClassName}}(...); + // $exception = new \RuntimeException('Job failed'); + + // Act + // $job->failed($exception); + + // Assert + $this->assertTrue(true); + } }