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:
2026-09-17 14:25:02 +07:00
parent 97f3833bf2
commit da1bd48444
33 changed files with 1004 additions and 594 deletions
@@ -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`.
@@ -1,154 +0,0 @@
# 10. Chiến Lược Đạt > 90% Code Coverage Trên SonarQube (Laravel & CakePHP)
Tài liệu này giải thích chi tiết cách cấu hình và kỹ thuật viết Unit & Integration Test để đạt chỉ số Line & Branch Coverage > 90% khi quét qua hệ thống phân tích chất lượng mã nguồn **SonarQube / SonarCloud** (chạy Local hoặc CI/CD).
---
## 1. Công Thức Tính Độ Phủ Của SonarQube
SonarQube tính toán chỉ số Coverage tổng hợp dựa trên cả dòng lệnh (Lines) lẫn điều kiện rẽ nhánh (Branches / Conditions):
$$\text{Coverage} = \frac{\text{Lines Covered} + \text{Branches Covered}}{\text{Total Lines} + \text{Total Branches}} \times 100\%$$
> ⚠️ **Lưu ý tối quan trọng**: Nếu một dòng code chạy qua nhưng chỉ thỏa mãn 1 trong 2 nhánh của toán tử ternary (`?:`), null coalescing (`??`) hoặc biểu thức logic (`&&`, `||`), SonarQube sẽ tính là **Partially Covered** (50% hoặc 75%) và làm tụt chỉ số Coverage tổng thể.
---
## 2. Cấu Hình Cốt Lõi: Scope & Exclusions
Tuyệt đối không đưa các file framework bootstrap, installer hoặc cấu hình vào phạm vi đo Coverage, vì chúng không chứa business logic và sẽ kéo tụt điểm số xuống dưới 80%.
### A. Cấu hình SonarQube cho CakePHP (`sonar-project.properties`)
```properties
sonar.projectKey=cakephp-app
sonar.projectName=CakePHP Application
sonar.sources=src
sonar.tests=tests
sonar.php.coverage.reportPaths=coverage.xml
sonar.php.tests.reportPath=phpunit-report.xml
# Inclusions: Tập trung đo các tầng nghiệp vụ cốt lõi
sonar.coverage.inclusions=src/Model/Table/**,src/Model/Entity/**,src/Controller/**,src/Service/**,src/Command/**,src/Controller/Component/**,src/Middleware/**,src/Mailer/**,src/Form/**,src/Model/Behavior/**,src/View/Helper/**
# Exclusions: Loại trừ framework boilerplate để đạt > 90%
sonar.coverage.exclusions=src/Console/Installer.php,src/Application.php,src/View/AppView.php,config/**,bin/**,plugins/**,tests/**,templates/**
sonar.cpd.exclusions=tests/**
```
### B. Cấu hình SonarQube cho Laravel (`sonar-project.properties`)
```properties
sonar.projectKey=laravel-app
sonar.projectName=Laravel Application
sonar.sources=app
sonar.tests=tests
sonar.php.coverage.reportPaths=coverage.xml
sonar.php.tests.reportPath=phpunit-report.xml
# Inclusions:
sonar.coverage.inclusions=app/Services/**,app/Actions/**,app/Http/Controllers/**,app/Http/Requests/**,app/Rules/**,app/Jobs/**,app/Policies/**,app/Models/**,app/Repositories/**,app/Console/Commands/**,app/Listeners/**,app/Notifications/**,app/Mail/**
# Exclusions: Loại trừ toàn bộ boilerplate, configs, migrations để đảm bảo Coverage > 90%
sonar.coverage.exclusions=app/Providers/**,app/Console/Kernel.php,app/Exceptions/Handler.php,app/Http/Kernel.php,app/Http/Middleware/TrustHosts.php,app/Http/Middleware/TrustProxies.php,app/Http/Middleware/VerifyCsrfToken.php,app/Http/Middleware/EncryptCookies.php,app/Http/Middleware/TrimStrings.php,app/Http/Middleware/PreventRequestsDuringMaintenance.php,bootstrap/**,config/**,database/**,tests/**
sonar.cpd.exclusions=tests/**
```
---
## 3. Cách Xuất Báo Cáo Coverage Cho SonarQube
Trong `phpunit.xml` (hoặc `phpunit.xml.dist`), kích hoạt xuất báo cáo định dạng Clover XML:
```xml
<coverage>
<report>
<clover outputFile="coverage.xml"/>
</report>
</coverage>
```
Chạy qua Smart Test Runner để tự động đo và xuất file:
```bash
./test-runner.sh --coverage-clover coverage.xml
```
Sau khi có file `coverage.xml`, quét SonarQube cục bộ bằng lệnh:
```bash
sonar-scanner
# Hoặc chạy container sonarsource/sonar-scanner-cli nếu chưa cài đặt CLI
```
---
## 4. Kỹ Thuật Đạt > 90% Branch Coverage Cho Mọi Dự Án PHP
### A. Toán tử Null Coalescing (`??`)
```php
$timeout = $config['timeout'] ?? 30;
```
- **Nhánh 1 (Left branch)**: Truyền `$config` có key `'timeout' => 60`.
- **Nhánh 2 (Right branch)**: Truyền `$config = []` (không có key `'timeout'`).
### B. Toán tử Ternary (`?:`) và Guard Clauses
```php
return $user->isVip() ? 0.2 : 0.05;
```
- **Nhánh 1**: `$user->isVip() === true`.
- **Nhánh 2**: `$user->isVip() === false`.
### C. Khối `try - catch - finally`
```php
try {
return $this->gateway->charge($amount);
} catch (PaymentException $e) {
Log::error($e->getMessage());
return false;
}
```
- **Bắt buộc**: Phải có ít nhất 1 test case ép mock ném `PaymentException` để kích hoạt toàn bộ khối `catch`.
### D. Điều kiện logic phức hợp (`&&`, `||`)
```php
if ($user->isActive() && $order->isPaid()) { ... }
```
- Cần tối thiểu 3 test cases:
1. `isActive = true` và `isPaid = true` (Vào `if`).
2. `isActive = false` (Short-circuit).
3. `isActive = true` nhưng `isPaid = false`.
---
## 5. Chiến Lược Đạt > 90% Riêng Cho Từng Tầng CakePHP
### 1. ORM Table (`validationDefault`):
- **Sai lầm phổ biến**: Chỉ test submit dữ liệu hợp lệ -> Bỏ sót 60% nhánh validation lỗi.
- **Kỹ thuật đạt > 90%**: 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`, `url`).
### 2. ORM Table (`buildRules`):
- Viết 1 test case lưu dữ liệu thỏa mãn rule.
- Viết 1 test case cố tình trùng lặp (đối với `isUnique`) hoặc ID không tồn tại (đối với `existsIn`) để kích hoạt nhánh rule validation failure.
### 3. Controller Actions:
- Viết GET request: Render view thành công (200).
- Viết POST/PUT request:
- **Happy Path**: Save thành công -> Redirect 302 + Flash Success.
- **Failure Path**: Save thất bại (dữ liệu thiếu/lỗi) -> Render 200 + Flash Error.
- **Not Found Path**: ID không tồn tại -> Bắt `RecordNotFoundException` (404).
- **Unauthorized Path**: Không có session -> Redirect Login.
### 4. Console Commands:
- Test chạy thành công: `assertExitSuccess()`.
- Test chạy lỗi (thiếu đối số bắt buộc hoặc option sai): `assertExitError()`.
- Test thông báo output: `assertOutputContains()`.
---
## 6. Checklist Kiểm Soát Chất Lượng Trước Khi Quét SonarQube
- [ ] File `sonar-project.properties` đã cấu hình đúng `sonar.coverage.inclusions` và `sonar.coverage.exclusions`.
- [ ] Báo cáo `coverage.xml` đã được sinh và không bị rỗng.
- [ ] Mọi phương thức public đều có ít nhất 1 test case Happy Path và 1 test case Failure/Edge.
- [ ] Tất cả các khối `try/catch` đều được chạy qua.
- [ ] Không có file test nào bị `SKIP` hoặc `FAIL`.
+2 -2
View File
@@ -155,7 +155,7 @@ protected array $fixtures = [
```bash
vendor/bin/phpunit --filter=ArticlesTableTest
```
4. **Đo Code Coverage cho SonarQube**:
4. **Đo Code Coverage**:
```bash
./test-runner.sh --coverage-clover coverage.xml
./test-runner.sh --coverage
```
@@ -109,7 +109,7 @@ Nhiều bài test chỉ viết:
$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%!
- **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ủ thực tế (Coverage) 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.
+1 -1
View File
@@ -116,7 +116,7 @@ class SendWelcomeEmailJobTest extends TestCase
### ❌ 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!
- **Hậu quả**: Độ phủ mã nguồn thực tế của Job chỉ đạt 1% - 10%, vì 90% dòng code nghiệp vụ 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.