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>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# 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. |
|
||||
Reference in New Issue
Block a user