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