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>
173 lines
5.9 KiB
Markdown
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. |
|