2045c856e9
- 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>
5.9 KiB
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
- 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. - 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')). - 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')). - 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. |