# 05. 12 Anti-Patterns Cần Tránh Khi Viết Laravel Unit Test Danh sách 12 sai lầm nghiêm trọng mà các công cụ AI và lập trình viên thường mắc phải khi viết Unit Test trong Laravel, kèm theo nguyên nhân và giải pháp chuẩn hóa. --- ### ❌ Anti-Pattern 1: Mock Chuỗi Phương Thức Eloquent (Mocking Query Chains) - **Sai lầm**: Cố gắng mock hàng loạt phương thức tĩnh hoặc chuỗi query của Eloquent: ```php // RẤT XẤU VÀ DỄ VỠ (BRITTLE) $builder = Mockery::mock(Builder::class); $builder->shouldReceive('where')->with('status', 'active')->andReturnSelf(); $builder->shouldReceive('orderBy')->with('created_at')->andReturnSelf(); $builder->shouldReceive('get')->andReturn(collect([$user])); ``` - **Hậu quả**: Khi chỉ cần đổi thứ tự `where` hoặc thêm một scope nhỏ trong code nguồn, test sẽ đỏ lòm dù nghiệp vụ hoàn toàn đúng. - **Giải pháp chuẩn**: 1. Nếu là Unit Test thuần: Trừu tượng hóa truy vấn vào **Repository** hoặc **Query Action** và mock Repository Interface đó. 2. Nếu bắt buộc test câu lệnh Query: Chuyển sang dùng **SQLite In-memory** và chạy query thật trên database test. --- ### ❌ Anti-Pattern 2: Lạm Dụng Database Thật Trong Unit Test - **Sai lầm**: Gọi `use RefreshDatabase;` và chèn hàng chục bản ghi Factory vào database cho một bài test chỉ kiểm tra logic tính toán toán học. - **Hậu quả**: Thời gian chạy test tăng vọt từ 5ms lên 300ms. Khi có 1,000 test cases, bộ test sẽ mất 5 phút thay vì 5 giây. - **Giải pháp chuẩn**: Chỉ dùng Database khi đối tượng kiểm thử là Repository, Eloquent Scope hoặc Custom Cast. Nếu là Service/Action/Rule, hãy mock data hoặc khởi tạo in-memory model bằng `$user = new User(['id' => 1])`. --- ### ❌ Anti-Pattern 3: Over-Mocking (Mock Cả Những Thứ Không Cần Thiết) - **Sai lầm**: Tạo mock cho cả DTO, Value Object, Collection, Carbon date object: ```php // SAI: Mock một DTO đơn giản $dto = Mockery::mock(UserRegistrationDTO::class); $dto->shouldReceive('getEmail')->andReturn('test@example.com'); ``` - **Giải pháp chuẩn**: Các đối tượng dữ liệu thuần (Data Holders / Value Objects) **phải được khởi tạo thật 100%**: ```php // ĐÚNG: Khởi tạo DTO thật $dto = new UserRegistrationDTO(email: 'test@example.com', name: 'John'); ``` --- ### ❌ Anti-Pattern 4: Assertions Rỗng / Vô Nghĩa - **Sai lầm**: ```php $service->doSomething(); $this->assertTrue(true); // Chỉ để pass test mà không xác thực kết quả ``` Hoặc chỉ kiểm tra kiểu dữ liệu `$this->assertIsArray($result);` mà không kiểm tra cấu trúc và giá trị bên trong. - **Giải pháp chuẩn**: Luôn assert giá trị chính xác (strict assertion): ```php $this->assertSame(expected: 250_000, actual: $result->finalAmount); ``` --- ### ❌ Anti-Pattern 5: Bỏ Quên Nhánh Exception & Edge Cases (Chỉ Test Happy Path) - **Sai lầm**: Chỉ viết 1 test case duy nhất khi mọi thứ hoàn hảo. Bỏ qua: - Input bị `null` hoặc chuỗi rỗng `""`. - External API trả về HTTP 401/403/500 hoặc Timeout. - Số âm, số 0 hoặc số nguyên quá lớn. - **Giải pháp chuẩn**: Luôn áp dụng tỷ lệ tối thiểu: 1 Happy Path + 2 Edge/Negative Cases + 1 Exception Case cho mỗi phương thức có logic phân nhánh. --- ### ❌ Anti-Pattern 6: Rò Rỉ Trạng Thái Giữa Các Test (State Leaks) - **Sai lầm**: Gán biến static, set mock Facade hoặc time travel bằng `Carbon::setTestNow()` trong một test case nhưng quên reset lại sau đó. - **Hậu quả**: Test case đứng sau bị fail ngẫu nhiên dù code của nó không sai (Flaky tests). - **Giải pháp chuẩn**: Luôn reset trong `tearDown()` hoặc dùng method `Carbon::setTestNow()` không tham số để trả về thời gian thực. --- ### ❌ Anti-Pattern 7: Dùng Sleep Trong Unit Test - **Sai lầm**: Viết `sleep(2);` để chờ queue xử lý hoặc chờ token hết hạn. - **Giải pháp chuẩn**: Dùng `Carbon::setTestNow()` để nhảy thời gian tức thì, hoặc dùng `Queue::fake()` để đồng bộ execution. --- ### ❌ Anti-Pattern 8: Kiểm Tra Chi Tiết Thực Thi (Testing Implementation Details) Thay Vì Hành Vi (Behavior) - **Sai lầm**: Kiểm tra xem hàm private được gọi thế nào, biến cục bộ bên trong hàm đổi tên gì. - **Giải pháp chuẩn**: Chỉ kiểm tra **Đầu vào (Input) -> Đầu ra (Output) hoặc Tác động ngoại vi công khai (Public Side-effects)**. Cho phép lập trình viên thoải mái refactor bên trong hàm mà không làm hỏng test. --- ### ❌ Anti-Pattern 9: Quên Assert Xác Thực Số Lần Gọi Mock (Missing Expectation Verification) - **Sai lầm**: Thiết lập mock nhưng không ghi rõ số lần: ```php $mailer->shouldReceive('send'); // Không biết gọi bao nhiêu lần, gọi 0 lần hay 100 lần vẫn pass! ``` - **Giải pháp chuẩn**: Luôn chỉ định rõ ràng: `->once()`, `->twice()`, `->never()`, hoặc `->times(3)`. --- ### ❌ Anti-Pattern 10: Khởi Tạo Trực Tiếp Bằng Từ Khóa `new` Trong Code Nguồn (Hard Dependency) - **Tình huống**: Code nguồn viết: ```php public function pay() { $stripe = new StripeClient('secret-key'); // Không thể mock từ ngoài vào $stripe->charge(); } ``` - **Cách xử lý**: - **Khuyến nghị**: Refactor đưa `StripeClient` vào Constructor Injection hoặc Service Provider. - **Nếu bắt buộc test mà không được sửa code nguồn**: Dùng Mockery Overload `Mockery::mock('overload:' . StripeClient::class)`. --- ### ❌ Anti-Pattern 11: Nhầm Lẫn Giữa Strict Assertion (`assertSame`) và Loose Assertion (`assertEquals`) - **Sai lầm**: `$this->assertEquals(0, 'any_string');` -> Trong PHP cũ có thể trả về `true` do ép kiểu lỏng lẻo. - **Giải pháp chuẩn**: Luôn ưu tiên dùng `$this->assertSame()` hoặc Pest `expect()->toBe()` để kiểm tra cả kiểu dữ liệu lẫn giá trị (`===`). --- ### ❌ Anti-Pattern 12: Test Quá Dài và Làm Quá Nhiều Thứ (God Test Method) - **Sai lầm**: Một test method dài 200 dòng, vừa tạo user, vừa cập nhật giỏ hàng, vừa thanh toán, vừa kiểm tra lịch sử. - **Giải pháp chuẩn**: Tách nhỏ thành từng test case độc lập. Mỗi test method chỉ nên dài từ 10 đến 30 dòng.