Files
agent-unit-testing/docs/08-test-data-factories-and-builders.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

172 lines
4.8 KiB
Markdown

# 08. Quản Lý Dữ Liệu Kiểm Thử: Factories, States & Test Builders (Pro Level)
Trong các dự án quy mô lớn, việc chuẩn bị dữ liệu kiểm thử (Test Fixtures / Test Data) chiếm tới 50% thời gian viết test. Tài liệu này hướng dẫn các kỹ thuật chuyên sâu cấp Senior/Principal để chuẩn bị dữ liệu siêu tốc, sạch và tái sử dụng cao.
---
## 1. Bí Mật Tốc Độ: `make()` vs `create()` Trong Model Factory
Trong Laravel Model Factory có 2 phương thức sinh dữ liệu chính:
| Phương thức | Hành vi | Tốc độ | Khi nào nên dùng? |
| :--- | :--- | :---: | :--- |
| `Model::factory()->create()` | Sinh model + **LƯU VÀO DATABASE** (`INSERT INTO`) | Chậm (~20-50ms) | **Feature Test** (Khi cần database thật để join/query) |
| `Model::factory()->make()` | Sinh model **TRONG BỘ NHỚ (RAM)**, KHÔNG chạm DB | Siêu nhanh (< 1ms) | **Unit Test** (Truyền model vào Service/Action/Policy) |
### 💡 Quy Tắc Vàng Cho Unit Test:
> **LUÔN DÙNG `make()` HOẶC `makeOne()` CHO UNIT TEST**. Trừ khi class bạn đang test bắt buộc phải query DB (như Repository), tuyệt đối không dùng `create()`.
```php
// ❌ CHẬM VÀ SAI NGUYÊN TẮC UNIT TEST:
$user = User::factory()->create(); // Chạy INSERT vào SQLite/MySQL
// ✅ CHUẨN PRO: Khởi tạo hoàn toàn trong RAM:
$user = User::factory()->make([
'id' => 1, // Tự gán ID giả lập nếu logic cần so sánh $user->id
]);
```
---
## 2. Tận Dụng Factory States Cho Các Kịch Bản Biên
Thay vì gán thủ công từng thuộc tính trong test method, hãy định nghĩa các **Factory States** mang ý nghĩa nghiệp vụ:
```php
// Trong database/factories/UserFactory.php
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
'role' => 'super_admin',
]);
}
public function banned(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'banned',
'banned_at' => now()->subDay(),
]);
}
```
### Sử dụng trong Unit Test:
```php
#[Test]
public function it_denies_access_for_banned_user(): void
{
// Cực kỳ trực quan, dễ đọc như văn xuôi
$bannedUser = User::factory()->banned()->make();
$this->assertFalse($this->policy->login($bannedUser));
}
```
---
## 3. Object Mother Pattern (Bộ Dựng Dữ Liệu Tập Trung)
Khi dự án có nhiều DTO hoặc Value Object phức tạp không dùng Eloquent Factory, hãy áp dụng pattern **Object Mother**:
```php
// tests/Fixtures/OrderDataMother.php
namespace Tests\Fixtures;
use App\DTOs\CreateOrderDTO;
use App\ValueObjects\Money;
class OrderDataMother
{
public static function standardOrder(int $total = 100_000): CreateOrderDTO
{
return new CreateOrderDTO(
customerId: 1,
items: [
['sku' => 'ITEM-01', 'qty' => 1, 'price' => $total]
],
totalAmount: new Money($total),
shippingAddress: '123 Nguyen Hue, Quan 1, TP.HCM'
);
}
public static function zeroAmountOrder(): CreateOrderDTO
{
return self::standardOrder(total: 0);
}
}
```
### Sử dụng trong Test:
```php
#[Test]
public function it_rejects_order_with_zero_amount(): void
{
$invalidOrder = OrderDataMother::zeroAmountOrder();
$this->expectException(InvalidOrderException::class);
$this->orderService->placeOrder($invalidOrder);
}
```
---
## 4. DTO Test Builder Pattern (Fluent API)
Khi một DTO có 10-15 trường và mỗi test case chỉ muốn thay đổi 1-2 trường:
```php
// tests/Builders/UserDTOBuilder.php
namespace Tests\Builders;
use App\DTOs\UserRegistrationDTO;
class UserDTOBuilder
{
private string $email = 'default@smartgift.vn';
private string $phone = '0912345678';
private string $name = 'Nguyen Van A';
private int $age = 25;
public static function aUser(): self
{
return new self();
}
public function withEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function underAge(int $age = 16): self
{
$this->age = $age;
return $this;
}
public function build(): UserRegistrationDTO
{
return new UserRegistrationDTO(
name: $this->name,
email: $this->email,
phone: $this->phone,
age: $this->age
);
}
}
```
### Sử dụng trong Test:
```php
#[Test]
public function it_throws_exception_when_user_is_under_18(): void
{
// Chỉ thay đổi đúng trường cần test, các trường khác giữ giá trị mặc định hợp lệ
$dto = UserDTOBuilder::aUser()->underAge(15)->build();
$this->expectException(UnderAgeRegistrationException::class);
$this->registrationAction->execute($dto);
}
```