feat: decouple Laravel and CakePHP installers, remove SonarQube artifacts, and enforce complete unit testing standards
- Decouple framework installation: add install-laravel.sh and install-cakephp.sh, isolate skills and docs per framework, and auto-clean leftover artifacts - Remove all SonarQube configs, templates, and references - Enforce comprehensive test completeness: 100% public methods, edge cases, full branch coverage, exception paths, and state assertions - Replace all placeholder assertions in PHPUnit and Pest templates with full realistic implementations - Rename and update 10-comprehensive-test-coverage-guide.md
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# 10. Chiến Lược Viết Test Đầy Đủ & Đạt Độ Phủ Toàn Diện (Laravel & CakePHP)
|
||||
|
||||
Tài liệu này cung cấp cẩm nang chi tiết về nguyên lý, phương pháp và kỹ thuật viết Unit & Integration Test **hoàn chỉnh, đầy đủ kịch bản, bao phủ 100% logic và mọi nhánh rẽ mã nguồn** cho cả hai framework Laravel và CakePHP.
|
||||
|
||||
---
|
||||
|
||||
## 1. Nguyên Tắc Cốt Lõi Của Việc Viết Test Đầy Đủ
|
||||
|
||||
Một file test được coi là **đầy đủ (comprehensive)** khi và chỉ khi:
|
||||
1. **100% Public Methods** của class đều có bộ test tương ứng (không bỏ sót helper methods, hooks, getters/setters có xử lý logic).
|
||||
2. **Line & Branch Coverage tối đa**: Chạy qua mọi dòng code và kích hoạt tất cả các nhánh rẽ điều kiện (`if/else`, `??`, `?:`, `switch`, `match`, `&&`, `||`).
|
||||
3. **Đầy đủ 4 nhóm kịch bản cho mỗi phương thức**:
|
||||
- **Happy Path**: Dữ liệu chuẩn, kết quả mong đợi, kiểu trả về đúng.
|
||||
- **Boundary & Edge Cases**: Giá trị biên, `null`, chuỗi rỗng, số 0, số âm, int max, mảng rỗng.
|
||||
- **Exception & Error Handling**: Ép mock ném ngoại lệ, kiểm tra xử lý trong khối `catch`, rollback dữ liệu.
|
||||
- **State & Side Effects**: Xác minh thay đổi dữ liệu trong Database, Event được dispatch, Job được queue, Mail được gửi.
|
||||
4. **Không viết test hình thức**: Tuyệt đối không dùng test rỗng (`assertTrue(true)`), không dùng `// TODO`, phải có strict assertion kiểm tra giá trị và trạng thái thật.
|
||||
|
||||
---
|
||||
|
||||
## 2. Cách Đo Độ Phủ (Code Coverage) Trực Tiếp Bằng PHPUnit & Pest
|
||||
|
||||
Không cần cấu hình phức tạp, bạn có thể đo trực tiếp độ phủ mã nguồn thông qua PHPUnit hoặc Pest Runner:
|
||||
|
||||
### A. Đo Coverage qua Smart Test Runner (Tự động):
|
||||
```bash
|
||||
# Hiển thị báo cáo độ phủ ngay trên Terminal
|
||||
./test-runner.sh --coverage
|
||||
|
||||
# Xuất báo cáo trực quan dạng HTML (mở file html-coverage/index.html trong trình duyệt)
|
||||
./test-runner.sh --coverage-html html-coverage
|
||||
|
||||
# Chặn test fail nếu độ phủ thấp hơn ngưỡng mong muốn (ví dụ 85%)
|
||||
./test-runner.sh --coverage --min=85
|
||||
```
|
||||
|
||||
### B. Chạy trực tiếp qua PHP CLI / Sail / Docker:
|
||||
```bash
|
||||
# Laravel (PHPUnit hoặc Pest)
|
||||
php artisan test --coverage --min=85
|
||||
./vendor/bin/pest --coverage --min=85
|
||||
|
||||
# CakePHP
|
||||
bin/cake test app --coverage-text
|
||||
./vendor/bin/phpunit --coverage-text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Kỹ Thuật Viết Test Bao Phủ 100% Nhánh Rẽ (Branch Coverage)
|
||||
|
||||
### A. Toán tử Null Coalescing (`??`)
|
||||
Toán tử `??` luôn tạo ra 2 nhánh logic ngầm:
|
||||
```php
|
||||
$timeout = $config['timeout'] ?? 30;
|
||||
```
|
||||
- **Kịch bản 1 (Left branch - Có giá trị)**: Truyền `$config` có key `'timeout' => 60` -> Assert kết quả nhận được `60`.
|
||||
- **Kịch bản 2 (Right branch - Null hoặc không tồn tại)**: Truyền `$config = []` -> Assert kết quả nhận được giá trị mặc định `30`.
|
||||
|
||||
### B. Toán tử Ternary (`?:`) và Guard Clauses
|
||||
```php
|
||||
return $user->isVip() ? 0.2 : 0.05;
|
||||
```
|
||||
- **Kịch bản 1 (Truthy branch)**: Giả lập `$user->isVip()` trả về `true` -> Assert kết quả `0.2`.
|
||||
- **Kịch bản 2 (Falsy branch)**: Giả lập `$user->isVip()` trả về `false` -> Assert kết quả `0.05`.
|
||||
|
||||
### C. Khối `try - catch - finally`
|
||||
```php
|
||||
try {
|
||||
return $this->gateway->charge($amount);
|
||||
} catch (PaymentException $e) {
|
||||
Log::error('Payment failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
```
|
||||
- **Happy Path**: Mock `$this->gateway->charge()` trả về mã giao dịch thành công.
|
||||
- **Exception Path (BẮT BUỘC)**: Giả lập Mock `$this->gateway->shouldReceive('charge')->andThrow(new PaymentException('Insufficient funds'))`.
|
||||
- Assert kết quả trả về `false`.
|
||||
- Assert log được ghi hoặc error event được kích hoạt.
|
||||
|
||||
### D. Điều Kiện Logic Phức Hợp (`&&`, `||`)
|
||||
```php
|
||||
if ($user->isActive() && $order->isPaid()) {
|
||||
$this->shipOrder($order);
|
||||
}
|
||||
```
|
||||
Cần tối thiểu 3 test cases để kiểm thử trọn vẹn:
|
||||
1. `isActive = true` VÀ `isPaid = true` -> Thực hiện ship hàng thành công.
|
||||
2. `isActive = false` -> Short-circuit, không ship hàng.
|
||||
3. `isActive = true` NHƯNG `isPaid = false` -> Không ship hàng.
|
||||
|
||||
### E. Cấu Trúc `switch` và `match`
|
||||
Mọi nhánh case và đặc biệt là nhánh `default` (hoặc `UnhandledMatchError`) đều phải có bài test kích hoạt tương ứng.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kỹ Thuật Viết Test Đầy Đủ Cho Từng Tầng Laravel
|
||||
|
||||
### 1. FormRequest & Custom Rules (`app/Http/Requests/`, `app/Rules/`):
|
||||
- **`authorize()`**: Test 2 trường hợp: User có quyền (`true`) và User không có quyền (`false`).
|
||||
- **`rules()`**: Bắt buộc test dữ liệu hợp lệ VÀ test vi phạm cho **từng rule riêng lẻ**:
|
||||
```php
|
||||
// Ví dụ test từng rule vi phạm để không sót nhánh validator:
|
||||
#[Test]
|
||||
public function validation_fails_when_email_is_missing(): void { ... }
|
||||
|
||||
#[Test]
|
||||
public function validation_fails_when_email_format_is_invalid(): void { ... }
|
||||
|
||||
#[Test]
|
||||
public function validation_fails_when_password_is_shorter_than_8_chars(): void { ... }
|
||||
```
|
||||
- **Custom Rules**: Test cả 2 trường hợp `passes() === true` và `passes() === false` (assert đúng thông điệp lỗi `message()`).
|
||||
|
||||
### 2. Services & Actions (`app/Services/`, `app/Actions/`):
|
||||
- Test kịch bản thành công với các loại đầu vào khác nhau (dùng `#[DataProvider]`).
|
||||
- Test kịch bản Mock các dịch vụ bên ngoài (Payment, SMS, Mail) bị lỗi ném Exception.
|
||||
- Test nghiệp vụ Database Transaction: Khi có lỗi phát sinh, dữ liệu phải được rollback sạch sẽ (`assertDatabaseMissing`).
|
||||
|
||||
### 3. Background Jobs (`app/Jobs/`):
|
||||
- **BẮT BUỘC test phương thức `handle()`**: Mock toàn bộ dependencies ngoài và verify logic thực thi cốt lõi.
|
||||
- **BẮT BUỘC test phương thức `failed(Throwable $e)`**: Kiểm tra hành vi dọn dẹp tài nguyên hoặc gửi thông báo cảnh báo khi job hết lượt retry.
|
||||
|
||||
### 4. Repositories & Query Builders (`app/Repositories/`):
|
||||
- Bắt buộc dùng **SQLite In-Memory (`:memory:`)** kết hợp trait `RefreshDatabase`.
|
||||
- Tạo dữ liệu mẫu bằng Factory và kiểm tra:
|
||||
- Query trả về đúng các bản ghi thỏa mãn điều kiện lọc.
|
||||
- Loại bỏ các bản ghi không thỏa mãn điều kiện.
|
||||
- Đúng thứ tự sắp xếp (`orderBy`).
|
||||
- Trả về Collection rỗng khi không có bản ghi nào khớp.
|
||||
- Tuyệt đối CẤM chỉ assert `assertInstanceOf(Builder::class)`!
|
||||
|
||||
### 5. Policies (`app/Policies/`):
|
||||
- Mọi action (`view`, `create`, `update`, `delete`, `restore`) đều phải test cả 2 kịch bản:
|
||||
- User có quyền (trả về `true`).
|
||||
- User không có quyền (trả về `false`).
|
||||
- Trường hợp tài khoản Admin được bypass / super-admin check.
|
||||
|
||||
---
|
||||
|
||||
## 5. Kỹ Thuật Viết Test Đầy Đủ Cho Từng Tầng CakePHP
|
||||
|
||||
### 1. ORM Tables (`src/Model/Table/`):
|
||||
- **`validationDefault()`**: Viết test case vi phạm cho từng rule:
|
||||
- Thiếu trường bắt buộc (`requirePresence`).
|
||||
- Chuỗi rỗng (`notEmptyString`).
|
||||
- Quá độ dài tối đa (`maxLength`).
|
||||
- Sai định dạng (`email`, `integer`, `numeric`, `url`).
|
||||
- **`buildRules()`**:
|
||||
- Test dữ liệu thỏa mãn rule -> Lưu thành công.
|
||||
- Test dữ liệu trùng lặp (`isUnique`) hoặc khóa ngoại không tồn tại (`existsIn`) -> Lưu thất bại, trả về rule errors.
|
||||
- **Custom Finders (`findPublished`, `findAuth`)**:
|
||||
- Test trả về đúng tập Entity cần tìm.
|
||||
- Test không trả về các Entity ở trạng thái khác (draft, archived).
|
||||
|
||||
### 2. Controllers (`src/Controller/`):
|
||||
- Bắt buộc dùng **`Cake\TestSuite\IntegrationTestTrait`**.
|
||||
- Bật token bảo vệ khi test POST/PUT:
|
||||
```php
|
||||
$this->enableCsrfToken();
|
||||
$this->enableSecurityToken();
|
||||
```
|
||||
- Phủ đầy đủ các kịch bản HTTP:
|
||||
- **GET (200)**: Render view thành công, assert view variables (`assertResponseOk()`).
|
||||
- **POST/PUT Success (302)**: Lưu thành công, assert chuyển hướng (`assertRedirect()`), flash message (`assertFlashMessage()`), và dữ liệu thực tế được lưu vào database.
|
||||
- **POST/PUT Validation Error (200)**: Form không hợp lệ, không redirect, hiển thị flash lỗi.
|
||||
- **Not Found (404)**: ID không tồn tại -> bắt `RecordNotFoundException`.
|
||||
- **Unauthorized (403 hoặc 302 Redirect Login)**: Truy cập khi chưa xác thực hoặc không đủ quyền.
|
||||
|
||||
### 3. Console Commands (`src/Command/`):
|
||||
- Bắt buộc dùng **`Cake\Console\TestSuite\ConsoleIntegrationTestTrait`**.
|
||||
- Test chạy thành công: `assertExitSuccess()`.
|
||||
- Test chạy lỗi (thiếu arguments, option sai): `assertExitError()`.
|
||||
- Test nội dung output trên màn hình: `assertOutputContains()`.
|
||||
|
||||
### 4. Mailers (`src/Mailer/`):
|
||||
- Bắt buộc dùng **`Cake\TestSuite\EmailTrait`**.
|
||||
- Assert người nhận (`assertMailSentTo`), tiêu đề (`assertMailContainsSubject`), và nội dung email mà không gửi mail thật ra ngoài.
|
||||
|
||||
---
|
||||
|
||||
## 6. Checklist Tự Kiểm Tra File Test Trước Khi Hoàn Tất
|
||||
|
||||
- [ ] **100% public methods** của class đã có bài test riêng.
|
||||
- [ ] Đầy đủ **Happy Path**, **Edge Cases** và **Exception Cases**.
|
||||
- [ ] Mọi toán tử `??`, `?:`, `if/else`, `try/catch` đều có test case cho tất cả các nhánh.
|
||||
- [ ] Không có bất kỳ dòng test nào dùng assertion rỗng hoặc `// TODO`.
|
||||
- [ ] Dữ liệu thay đổi trong DB hoặc các side effect (Event, Job, Mail) được assert chặt chẽ.
|
||||
- [ ] Test chạy PASS 100% khi thực thi `./test-runner.sh`.
|
||||
Reference in New Issue
Block a user