Files
agent-unit-testing/docs/03-mocking-strategies.md
T
Pham-Tan-Thien 2045c856e9 feat: complete Smartgift AI Laravel unit testing toolkit
- Add 10 core testing guides (setup, AAA principles, PHPUnit/Pest, mocking, anti-patterns, PHP 8.4+, debugging, data factories, advanced edge cases)
- Add 13 component testing guides covering all Laravel layers
- Add standard templates for PHPUnit 10/11 and Pest v2/v3 including ArchitectureTest
- Add Docker smart test runner, dependency installer, and CI/CD templates (GitHub Actions, GitLab CI)
- Add 4 Claude Code skills (laravel-test-setup, laravel-unit-test with --all mode, laravel-test-review, laravel-mock-helper)
- Add single unified installer script (sg-install-unit-testing.sh) with global support, CLI runners, and 1-click updater (sg-test-update)
- Add comprehensive documentation (README.md, USAGE-GUIDE.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 16:41:37 +07:00

5.9 KiB

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:

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

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

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)

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)

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)

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

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:

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.