# 07. Troubleshooting & Debugging Guide for Laravel Unit Tests Cẩm nang chẩn đoán và khắc phục 10 lỗi phổ biến nhất khi viết và chạy Unit Test trong các dự án Laravel. --- ## 1. Lỗi: `Mockery\Exceptions\InvalidCountException: Method ... was expected to be called 1 times but called 0 times` - **Nguyên nhân**: - Bạn khai báo `->once()` cho một method mock, nhưng luồng code trong hàm thực tế bị rẽ nhánh sớm (early return) hoặc ném exception trước khi chạm tới method đó. - Tên method trong `shouldReceive('tên_hàm')` bị sai chính tả. - **Cách khắc phục**: 1. Kiểm tra điều kiện `if/else` trước lệnh gọi: Đảm bảo dữ liệu đầu vào trong `// Arrange` thỏa mãn để code đi vào nhánh gọi mock. 2. Kiểm tra log hoặc dùng `$this->withoutExceptionHandling()` (nếu trong Feature test) để xem có exception nào bị nuốt không. --- ## 2. Lỗi: `BadMethodCallException: Method Mockery_...::someMethod() does not exist on this mock object` - **Nguyên nhân**: Code nguồn gọi phương thức `someMethod()`, nhưng bạn chưa định nghĩa nó trong mock (`shouldReceive('someMethod')`). - **Cách khắc phục**: - Khai báo bổ sung kỳ vọng cho method đó, hoặc nếu method đó không ảnh hưởng tới kết quả bài test, dùng `->shouldIgnoreMissing()` hoặc tạo `Spy` thay vì `Mock`: ```php $loggerMock = Mockery::spy(LoggerInterface::class); ``` --- ## 3. Lỗi: `Target [App\Contracts\PaymentInterface] is not instantiable while building [App\Services\CheckoutService]` - **Nguyên nhân**: Code nguồn cố gắng resolve Interface từ Service Container nhưng Interface đó chưa được bind trong `AppServiceProvider` trong môi trường test. - **Cách khắc phục**: - Trong Unit Test: **Khởi tạo trực tiếp bằng từ khóa `new`** và truyền mock vào constructor thay vì gọi `app(CheckoutService::class)`: ```php // ĐÚNG CHO UNIT TEST: $service = new CheckoutService($mockPayment); // NẾU CẦN RESOLVE TỪ CONTAINER: $this->app->instance(PaymentInterface::class, $mockPayment); $service = $this->app->make(CheckoutService::class); ``` --- ## 4. Lỗi: `SQLSTATE[HY000]: General error: 1 no such table: users` - **Nguyên nhân**: Test đang chạy trên SQLite `:memory:` nhưng chưa chạy migration, hoặc trong Unit test bạn vô tình gọi Eloquent Model thật mà không dùng trait `RefreshDatabase`. - **Cách khắc phục**: 1. **Nếu là Unit Test thuần**: Bạn không nên chạm vào DB! Hãy mock Repository hoặc khởi tạo in-memory model `$user = new User(['id' => 1])`. 2. **Nếu là Repository/Query Test bắt buộc cần DB**: Thêm trait `use RefreshDatabase;` vào đầu class test. --- ## 5. Lỗi: `SQLSTATE[HY000]: General error: 1 no such function: JSON_CONTAINS / DATE_FORMAT` - **Nguyên nhân**: Code nguồn sử dụng hàm SQL đặc thù của MySQL, nhưng môi trường test chạy trên SQLite In-Memory. - **Cách khắc phục**: - Đăng ký hàm giả lập trong `setUp()`: ```php if (DB::connection() instanceof \Illuminate\Database\SQLiteConnection) { DB::connection()->getPdo()->sqliteCreateFunction('JSON_CONTAINS', function ($json, $val) { $data = json_decode($json, true) ?? []; return in_array(trim($val, '"'), $data) ? 1 : 0; }, 2); } ``` - Hoặc chuyển kết nối test trong `phpunit.xml` sang một database MySQL test độc lập trên Docker. --- ## 6. Lỗi: `Illuminate\Database\QueryException: Integrity constraint violation: 19 FOREIGN KEY constraint failed` - **Nguyên nhân**: SQLite bật kiểm tra khóa ngoại và bản ghi cha không tồn tại. - **Cách khắc phục**: - Tạm thời tắt kiểm tra foreign key trong test setup: ```php use Illuminate\Support\Facades\Schema; protected function setUp(): void { parent::setUp(); Schema::disableForeignKeyConstraints(); } ``` - Hoặc tạo các Model quan hệ bằng Factory: `User::factory()->has(Post::factory())->create();`. --- ## 7. Lỗi: Flaky Test (Chạy riêng lẻ thì Pass, chạy chung cả bộ test thì Fail) - **Nguyên nhân**: **Rò rỉ trạng thái (State Leakage)** giữa các bài test: - Dùng `Carbon::setTestNow()` nhưng quên reset. - Sử dụng static variable hoặc singleton service lưu cache trong memory. - Mock Facade tĩnh chưa được giải phóng. - **Cách khắc phục**: - Đảm bảo trong `tearDown()` có gọi: ```php protected function tearDown(): void { Carbon::setTestNow(); // Reset thời gian thực Mockery::close(); parent::tearDown(); } ``` --- ## 8. Lỗi: `Error: Typed property ... must not be accessed before initialization` - **Nguyên nhân**: Thuộc tính khai báo type (ví dụ `private UserService $service;`) nhưng trong method `setUp()` bị sót logic gán giá trị, hoặc test method chạy trước khi `setUp()` hoàn tất. - **Cách khắc phục**: Kiểm tra phương thức `setUp()` và đảm bảo mọi typed property đều được khởi tạo instance trước khi method test chạy. --- ## 9. Lỗi: `Class "App\Http\Controllers\Controller" not found` - **Nguyên nhân**: Laravel 11 đã loại bỏ base `Controller.php` mặc định nếu dự án tạo mới tối giản, hoặc namespace bị sai lệch. - **Cách khắc phục**: Kiểm tra class controller đích kế thừa class nào để import chính xác. --- ## 10. Lỗi: `Session store not set on request` - **Nguyên nhân**: Code nguồn gọi `request()->session()` hoặc `$request->session()->get(...)` trong Unit test khi chưa có middleware session. - **Cách khắc phục**: - Gắn session giả lập vào Request: ```php use Illuminate\Session\ArraySessionHandler; use Illuminate\Session\Store; $session = new Store('test_session', new ArraySessionHandler(10)); $request->setLaravelSession($session); ```