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>
172 lines
4.8 KiB
Markdown
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);
|
|
}
|
|
```
|