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>
195 lines
5.9 KiB
Markdown
195 lines
5.9 KiB
Markdown
# 09. Các Tình Huống Kỹ Thuật Đặc Biệt (Advanced Edge Cases)
|
|
|
|
Tài liệu hướng dẫn xử lý 7 tình huống kỹ thuật phức tạp thường gặp trong các dự án Laravel thực tế mà lập trình viên và AI hay bị lúng túng.
|
|
|
|
---
|
|
|
|
## 1. Mock `DB::transaction()` Trong Unit Test (Không Cần Database Thật)
|
|
|
|
Nhiều Service bọc logic thanh toán/tạo đơn trong `DB::transaction(function () { ... })`. Khi viết Unit Test cô lập (không kết nối DB), nếu không mock lệnh này, code sẽ văng lỗi connection.
|
|
|
|
### ✅ Giải pháp chuẩn:
|
|
Sử dụng `DB::shouldReceive('transaction')` và thực thi ngay closure truyền vào:
|
|
|
|
```php
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
#[Test]
|
|
public function it_executes_logic_inside_database_transaction(): void
|
|
{
|
|
// Giả lập DB::transaction để chạy trực tiếp callback bên trong
|
|
DB::shouldReceive('transaction')
|
|
->once()
|
|
->andReturnUsing(function (callable $callback) {
|
|
return $callback();
|
|
});
|
|
|
|
// Act
|
|
$result = $this->orderService->createOrder($orderDTO);
|
|
|
|
// Assert
|
|
$this->assertNotNull($result);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Kiểm Thử Trait Trong Sự Cô Lập (Testing Traits in Isolation)
|
|
|
|
Khi logic nghiệp vụ nằm trong một Trait (ví dụ: `GeneratesInvoiceCodeTrait`, `HasSlugTrait`), làm sao để test mà không cần khởi tạo một Model hoặc Service cụ thể?
|
|
|
|
### ✅ Giải pháp chuẩn: Dùng Anonymous Class
|
|
```php
|
|
#[Test]
|
|
public function it_generates_unique_invoice_code_from_trait(): void
|
|
{
|
|
// Tạo một anonymous class sử dụng Trait cần test
|
|
$instance = new class {
|
|
use GeneratesInvoiceCodeTrait;
|
|
|
|
// Bổ sung các method/property trừu tượng nếu trait yêu cầu
|
|
public function prefix(): string {
|
|
return 'INV';
|
|
}
|
|
};
|
|
|
|
$code = $instance->generateInvoiceCode(123);
|
|
|
|
$this->assertMatchesRegularExpression('/^INV-123-[A-Z0-9]{4}$/', $code);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Thao Tác Cấu Hình (`config()`) & Biến Môi Trường Trong Test
|
|
|
|
Nhiều class đọc cấu hình qua `config('services.stripe.secret')`.
|
|
|
|
### ✅ Quy tắc chuẩn:
|
|
- **KHÔNG BAO GIỜ** sửa file `.env` thật trong lúc test.
|
|
- Sử dụng `Config::set()` trực tiếp trong phần `// Arrange`:
|
|
```php
|
|
use Illuminate\Support\Facades\Config;
|
|
|
|
#[Test]
|
|
public function it_uses_configured_vat_rate(): void
|
|
{
|
|
// Gán tạm thời cấu hình cho bài test này
|
|
Config::set('finance.vat_rate', 0.08); // 8%
|
|
|
|
$tax = $this->taxService->calculate(100_000);
|
|
$this->assertSame(8_000.0, $tax);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Kiểm Thử Upload File & Multi-part Request (`UploadedFile::fake()`)
|
|
|
|
Kiểm thử các service xử lý file ảnh, tài liệu PDF, file CSV mà không cần file thật trên ổ cứng:
|
|
|
|
```php
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
#[Test]
|
|
public function it_uploads_and_validates_pdf_invoice(): void
|
|
{
|
|
Storage::fake('documents');
|
|
|
|
// Tạo file PDF giả lập trong bộ nhớ
|
|
$fakePdf = UploadedFile::fake()->create('invoice.pdf', 500, 'application/pdf');
|
|
|
|
$uploadedPath = $this->documentService->storeInvoice($fakePdf);
|
|
|
|
Storage::disk('documents')->assertExists($uploadedPath);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_rejects_file_exceeding_max_size(): void
|
|
{
|
|
// Giả lập file ảnh vượt quá 10MB (11,000 KB)
|
|
$heavyImage = UploadedFile::fake()->image('huge.jpg')->size(11_000);
|
|
|
|
$this->expectException(FileTooLargeException::class);
|
|
$this->avatarService->upload($heavyImage);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Kiểm Thử Lỗi Serialization Của Queueable Jobs (`SerializesModels`)
|
|
|
|
Trong Laravel, khi truyền một Eloquent Model vào Job, Laravel sẽ serialize model bằng ID của nó. Nếu truyền một Model chưa được lưu vào DB (ID = null), Job sẽ crash trong worker thực tế.
|
|
|
|
### ✅ Cách kiểm tra độ an toàn của Job Payload:
|
|
```php
|
|
#[Test]
|
|
public function it_can_be_serialized_and_unserialized_safely(): void
|
|
{
|
|
$user = User::factory()->make(['id' => 101]);
|
|
$job = new SendWelcomeEmailJob($user);
|
|
|
|
// Thử serialize và unserialize giống hệt cơ chế của Queue Worker
|
|
$serialized = serialize($job);
|
|
$unserializedJob = unserialize($serialized);
|
|
|
|
$this->assertInstanceOf(SendWelcomeEmailJob::class, $unserializedJob);
|
|
$this->assertSame(101, $unserializedJob->user->id);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Xử Lý Giả Lập Session & Authentication Trong Unit Test Thuần
|
|
|
|
Khi class nguồn gọi `auth()->user()`, `auth()->id()` hoặc `session()->get()`:
|
|
|
|
```php
|
|
use Illuminate\Support\Facades\Auth;
|
|
use App\Models\User;
|
|
|
|
#[Test]
|
|
public function it_retrieves_data_for_authenticated_user(): void
|
|
{
|
|
// Arrange: Giả lập user đã đăng nhập vào Auth Facade
|
|
$mockUser = new User(['id' => 99, 'name' => 'Thien Phan']);
|
|
Auth::shouldReceive('user')->andReturn($mockUser);
|
|
Auth::shouldReceive('id')->andReturn(99);
|
|
Auth::shouldReceive('check')->andReturnTrue();
|
|
|
|
// Act
|
|
$result = $this->userProfileService->getCurrentUserProfile();
|
|
|
|
// Assert
|
|
$this->assertSame('Thien Phan', $result->name);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Kiểm Thử Sự Kiện Đa Kênh & Tránh Cascading Events
|
|
|
|
Khi một hành động kích hoạt Event, và Event đó lại kích hoạt nhiều Listener khác có thể ghi dữ liệu hoặc gửi mail:
|
|
|
|
```php
|
|
use Illuminate\Support\Facades\Event;
|
|
use App\Events\UserRegisteredEvent;
|
|
|
|
#[Test]
|
|
public function it_dispatches_registered_event_without_firing_listeners(): void
|
|
{
|
|
// Chặn tất cả Listener chạy ngầm, chỉ xác nhận Event được bắn ra
|
|
Event::fake([
|
|
UserRegisteredEvent::class
|
|
]);
|
|
|
|
$this->registrationService->register($userData);
|
|
|
|
// Xác nhận event được dispatch kèm payload chuẩn
|
|
Event::assertDispatched(UserRegisteredEvent::class, function ($event) use ($userData) {
|
|
return $event->userEmail === $userData['email'];
|
|
});
|
|
}
|
|
```
|