Files
agent-unit-testing/docs/03-mocking-strategies.md
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

173 lines
5.9 KiB
Markdown

# 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. |