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

4.8 KiB

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().

// ❌ 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ụ:

// 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:

#[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:

// 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:

#[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:

// 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:

#[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);
}