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 <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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(...)`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()`).
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user