# 03. Mocking & Test Doubles Strategies in Laravel Cẩm nang toàn diện về chiến lược giả lập đối tượng (Test Doubles): Khi nào dùng Mockery, khi nào dùng Laravel Fakes, và cách xử lý Static Facades. --- ## 1. Phân loại Test Doubles: Mock, Stub, Spy và Dummy 1. **Dummy**: Đối tượng truyền vào chỉ để lấp đầy tham số (e.g. `$user = new User()`), không được gọi phương thức nào. 2. **Stub**: Trả về dữ liệu cố định định sẵn, không quan tâm nó bị gọi bao nhiêu lần (e.g. `->andReturn('success')`). 3. **Mock**: Kiểm tra hành vi (Behavior Verification). Bắt buộc phải được gọi với đúng tham số và số lần nhất định (e.g. `->once()->with('param')`). 4. **Spy**: Ghi nhận lại các lệnh gọi và kiểm tra lại sau khi hành động đã diễn ra (e.g. `->shouldHaveReceived(...)`). --- ## 2. Chiến lược 1: Dùng Mockery cho Dependency Injection Áp dụng cho các Service, Repository, Gateway được truyền qua Constructor hoặc Method Injection. ### Quy tắc Mockery chuẩn: ```php // 1. Tạo Mock cho Interface (Luôn ưu tiên mock Interface thay vì Concrete Class) $paymentGateway = Mockery::mock(PaymentGatewayInterface::class); // 2. Thiết lập kỳ vọng chính xác $paymentGateway->shouldReceive('charge') ->once() // Bắt buộc phải gọi đúng 1 lần ->with(50000, 'VND') // Tham số truyền vào phải khớp chính xác ->andReturn(new PaymentResponse(status: 'SUCCESS', transactionId: 'TXN_123')); // 3. Truyền Mock vào Service $checkoutService = new CheckoutService($paymentGateway); $result = $checkoutService->processOrder($order); // 4. Dọn dẹp Mockery (Trong Base TestCase Laravel đã tự gọi Mockery::close()) ``` ### Trường hợp Exception: ```php $paymentGateway->shouldReceive('charge') ->once() ->andThrow(new PaymentFailedException('Insufficient funds')); ``` --- ## 3. Chiến lược 2: Laravel Built-in Fakes Laravel cung cấp sẵn hệ thống Fakes cực mạnh. **Luôn ưu tiên dùng Laravel Fakes thay vì tự dùng Mockery cho các thành phần sau:** ### A. `Http::fake()` (Test gọi API bên thứ 3) ```php use Illuminate\Support\Facades\Http; Http::fake([ // Giả lập endpoint cụ thể 'api.stripe.com/*' => Http::response(['id' => 'ch_123', 'status' => 'succeeded'], 200), // Giả lập lỗi 500 'api.github.com/*' => Http::response(['message' => 'Server Error'], 500), // Giả lập kết nối timeout 'api.slow-service.com/*' => Http::failedConnection(), ]); // Assert sau khi Act Http::assertSent(function ($request) { return $request->url() === 'https://api.stripe.com/v1/charges' && $request['amount'] == 1000; }); ``` ### B. `Queue::fake()` & `Bus::fake()` (Test đẩy công việc vào hàng đợi) ```php use Illuminate\Support\Facades\Queue; Queue::fake(); // Act: Thực hiện nghiệp vụ $registrationService->register($userData); // Assert: Job SendWelcomeEmail được dispatch với đúng user Queue::assertPushed(SendWelcomeEmailJob::class, function ($job) use ($userData) { return $job->email === $userData['email']; }); // Assert: Đảm bảo job nguy hiểm KHÔNG bị gọi Queue::assertNotPushed(DeleteAccountJob::class); ``` ### C. `Event::fake()` (Test phát sinh sự kiện) ```php use Illuminate\Support\Facades\Event; Event::fake([ OrderShippedEvent::class, // Chỉ fake event này, các event khác vẫn giữ nguyên ]); // Assert Event::assertDispatched(OrderShippedEvent::class); ``` ### D. `Storage::fake()` (Test upload / xóa file) ```php use Illuminate\Support\Facades\Storage; use Illuminate\Http\UploadedFile; Storage::fake('avatars'); $file = UploadedFile::fake()->image('profile.jpg', 400, 400)->size(500); $avatarService->updateAvatar($userId, $file); // Assert file đã lưu trên disk avatars Storage::disk('avatars')->assertExists($file->hashName()); ``` --- ## 4. Chiến lược 3: Xử lý Static Facades & Time ### A. Facades: Cache & Log ```php use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; // Test Cache Cache::shouldReceive('get') ->once() ->with('user_settings_1') ->andReturn(['theme' => 'dark']); // Test Log Log::shouldReceive('error') ->once() ->withArgs(function ($message, $context) { return str_contains($message, 'Payment failed'); }); ``` ### B. Xử lý Thời gian với Carbon (Time-travel) Không dùng `sleep()` trong Unit Test! Dùng tính năng đóng băng thời gian của Carbon: ```php use Illuminate\Support\Carbon; // Đóng băng thời gian tại một mốc cụ thể Carbon::setTestNow('2026-09-07 10:00:00'); // Chạy logic nghiệp vụ phụ thuộc ngày giờ $isExpired = $subscriptionService->isExpired($subscription); $this->assertFalse($isExpired); // Nhảy đến tương lai 30 ngày Carbon::setTestNow('2026-10-08 10:00:00'); $this->assertTrue($subscriptionService->isExpired($subscription)); // Luôn reset lại thời gian trong tearDown nếu cần Carbon::setTestNow(); ``` --- ## 5. Quy tắc Vàng: "Cái gì CẦN Mock và cái gì KHÔNG ĐƯỢC Mock?" | Thành phần | Có Mock không? | Giải thích | | :--- | :---: | :--- | | **Interface / External Gateway** | ✅ CÓ | Cần cô lập các service ngoài, network calls | | **Email, SMS, Queue, Storage** | ✅ CÓ | Dùng Laravel Fakes (`Mail::fake()`, `Queue::fake()`) | | **DTO (Data Transfer Object)** | ❌ KHÔNG | DTO chỉ chứa dữ liệu thuần. Hãy khởi tạo `new UserDTO(...)` thật! | | **Value Objects** | ❌ KHÔNG | Ví dụ `Money`, `EmailAddress`. Dùng object thật | | **Collection** | ❌ KHÔNG | Khởi tạo `collect([1, 2, 3])` thật | | **Eloquent Model chuỗi (Chain)**| ⚠️ HẠN CHẾ | Không mock `User::where()->with()->get()`. Thay bằng In-memory SQLite hoặc Repository pattern. |