Files
agent-unit-testing/docs/10-comprehensive-test-coverage-guide.md
Pham-Tan-Thien da1bd48444 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
2026-09-17 14:25:02 +07:00

9.6 KiB

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):

# 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:

# 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:

$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

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

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 (&&, ||)

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ẻ:
    // 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:
    $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.