# 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`.