Files
agent-unit-testing/docs/09-advanced-edge-cases.md
T
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

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'];
});
}
```