Laravel 工厂:使用 Laravel 工厂模型构建真实的测试数据
在使用 Laravel 开发应用程序时,测试工作流中最常见的瓶颈之一是生成有意义的、真实的数据。Laravel 工厂是定义创建 Eloquent 模型实例蓝图的类,使用 Faker PHP 库生成随机但结构有效的属性值——使开发人员能够填充数据库并编写隔离测试,而无需手动构建数据固件。
与静态 SQL 种子文件或硬编码数组不同,工厂是可组合的、有状态的,并且支持关联关系。它们直接与 PHPUnit 和 Pest 测试套件集成,支持属性的延迟求值,并且可以从单个模型实例扩展到单个方法链中的数千条记录。如果您在 VPS 托管环境中运行 Laravel,工厂在 CI/CD 流水线运行、暂存环境重置以及需要可重复、受控数据生成的负载测试场景中尤为重要。
什么是 Laravel 工厂及其重要性
Laravel 工厂在 Laravel 8 中进行了根本性的重新设计。旧的基于闭包的 `$factory->define()` 方法被专用的 PHP 类所取代,这些类继承自 `IlluminateDatabaseEloquentFactoriesFactory`。这一架构转变引入了类型安全、IDE 自动补全以及工厂逻辑与模型定义之间更清晰的分离。
每个工厂类实现一个 `definition()` 方法,该方法返回一个模型属性的关联数组。工厂自动解析一个 `FakerGenerator` 实例,可通过 `$this->faker` 访问,支持超过 200 个区域感知的数据提供者——从 `name()` 和 `safeEmail()` 到 `iban()`、`latitude()`、`uuid()` 和 `creditCardNumber()`。
现代工厂系统的主要功能:
- 流式方法链,用于数量、状态和关联关系配置
- 延迟属性解析——`definition()` 内的闭包在每个实例中重新求值
- 工厂状态,用于建模特定领域的变体(例如,已暂停账户、已验证用户)
- 关联关系工厂,在需要时递归创建父模型
- 序列,用于循环遍历预定义的属性集
- `make()` 与 `create()`,分别用于内存实例与持久化实例
前提条件
在实现工厂之前,请确保您的环境满足以下要求:
- Laravel 9 或更高版本(Laravel 8 兼容,但缺少一些较新的序列功能)
- PHP 8.0 或更高版本
- 在 `.env` 中配置的数据库连接(MySQL、PostgreSQL 或用于内存测试的 SQLite)
- `laravel/framework` 包,作为 `fakerphp/faker` 的依赖项随附
- 具有相应迁移文件的 Eloquent 模型
对于在托管基础设施上运行 Laravel 的团队,带 cPanel 的 VPS 提供了一个便捷的环境,可从统一界面管理应用程序堆栈和数据库服务。
第 1 步:生成工厂类
使用 Artisan CLI 生成工厂文件:
“`bash
php artisan make:factory UserFactory
“`
这将创建 `database/factories/UserFactory.php`。如果您想自动将工厂与模型关联,请传递 `–model` 标志:
“`bash
php artisan make:factory UserFactory –model=User
“`
Laravel 通过命名约定解析工厂与模型的绑定:`UserFactory` 映射到 `AppModelsUser`。您可以通过显式设置 `protected $model` 属性来覆盖此设置,当您的模型位于默认 `AppModels` 命名空间之外时,这一点至关重要。
第 2 步:定义工厂蓝图
打开 `database/factories/UserFactory.php` 并定义 `definition()` 方法:
“`php
<?php
namespace DatabaseFactories;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
use IlluminateSupportStr;
use IlluminateSupportFacadesHash;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('password'),
'remember_token' => Str::random(10),
];
}
}
“`
属性级别说明:
- `$this->faker->unique()->safeEmail()`——`unique()` 修饰符维护一个每次请求的唯一性注册表。如果您耗尽了可用的唯一值(在非常大的数据集中很少见但可能发生),Faker 会抛出 `OverflowException`。使用 `$this->faker->unique(true)` 重置以清除缓存。
- `Hash::make('password')` 优于直接使用 `bcrypt()`,因为它遵循应用程序配置的哈希驱动程序(bcrypt、argon2i、argon2id)。
- `email_verified_at => now()` 将用户标记为已验证。省略此字段或将其设置为 `null` 以模拟未验证账户——这是一种常见的状态变体。
第 3 步:创建模型实例
3.1 持久化单条记录
“`php
$user = AppModelsUser::factory()->create();
“`
这将执行一条 `INSERT` 语句并返回一个已填充的 `User` Eloquent 模型。返回的实例反映实际的数据库状态,包括任何数据库级别的默认值或触发器。
3.2 持久化多条记录
“`php
$users = AppModelsUser::factory()->count(10)->create();
“`
返回一个包含 10 个 `User` 实例的 `IlluminateDatabaseEloquentCollection`。每条记录都会收到独立生成的 Faker 值——它们不是单个实例的副本。
3.3 不持久化的内存实例
“`php
$user = AppModelsUser::factory()->make();
“`
`make()` 方法实例化模型并填充其属性,而不接触数据库。这非常适合用于验证模型行为、属性转换或访问器/修改器逻辑的单元测试——保持测试快速且独立于数据库。
3.4 覆盖特定属性
`create()` 和 `make()` 都接受属性覆盖数组:
“`php
$user = AppModelsUser::factory()->create([
'email' => 'specific@example.com',
'name' => 'Jane Doe',
]);
“`
覆盖值优先于 `definition()` 中的值。当测试依赖于特定的已知属性值而非随机值时,这是正确的模式。
第 4 步:工厂状态
状态是对基础工厂定义的命名修改。它们允许您建模不同的领域条件,而无需复制整个工厂。
4.1 定义状态
“`php
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
'role' => 'administrator',
]);
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'suspended_at' => now(),
'is_active' => false,
]);
}
“`
4.2 应用状态
“`php
// Single state
$adminUser = AppModelsUser::factory()->admin()->create();
// Stacked states — fully composable
$suspendedAdmin = AppModelsUser::factory()->admin()->suspended()->create();
“`
状态按链式调用的顺序求值。后续状态会覆盖前面状态中冲突的键,为您提供可预测的、分层的属性解析。
第 5 步:用于循环属性值的序列
当您需要在一组定义的值之间交替而不是使用随机值时,请使用 `Sequence`:
“`php
use IlluminateDatabaseEloquentFactoriesSequence;
$users = AppModelsUser::factory()
->count(6)
->state(new Sequence(
['role' => 'editor'],
['role' => 'viewer'],
['role' => 'moderator'],
))
->create();
“`
这会循环遍历序列数组,按顺序分配角色。对于 6 个用户,每个角色被分配两次。序列对于测试分页、基于角色的访问控制以及依赖于多样但受控数据分布的 UI 渲染逻辑非常有价值。
第 6 步:关联关系工厂
6.1 定义 Belongs-To 关联关系
在 `PostFactory.php` 中,直接将父工厂作为属性值引用:
“`php
<?php
namespace DatabaseFactories;
use AppModelsPost;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => $this->faker->sentence(),
'body' => $this->faker->paragraphs(3, true),
'slug' => $this->faker->unique()->slug(),
];
}
}
“`
当 `user_id` 设置为 `User::factory()` 时,Laravel 会延迟其求值。如果您在不提供 `user_id` 的情况下调用 `Post::factory()->create()`,则会自动创建一个新的 `User` 并使用其主键。如果您提供了现有用户,则完全跳过嵌套工厂。
6.2 附加到现有父模型
“`php
$user = AppModelsUser::factory()->create();
$posts = AppModelsPost::factory()->count(5)->for($user)->create();
“`
`for()` 方法将 `user_id` 外键设置为提供的模型的主键,防止不必要的用户创建。当您的测试中已经有特定用户时,这是正确的模式。
6.3 Has-Many 关联关系
“`php
$userWithPosts = AppModelsUser::factory()
->has(AppModelsPost::factory()->count(3), 'posts')
->create();
“`
或使用魔术 `hasPosts()` 简写(通过模型上的关联关系方法名称解析):
“`php
$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();
“`
这将在单个原子操作中创建一个用户和三个关联的文章——所有外键都正确解析。
6.4 Many-to-Many 关联关系
“`php
$user = AppModelsUser::factory()
->hasAttached(
AppModelsRole::factory()->count(2),
['assigned_at' => now()]
)
->create();
“`
`hasAttached()` 方法处理数据透视表的插入,包括您需要填充的任何额外数据透视属性。
第 7 步:在测试中使用工厂
7.1 带数据库断言的功能测试
“`php
use IlluminateFoundationTestingRefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created_with_factory(): void
{
$user = AppModelsUser::factory()->create();
$this->assertDatabaseHas('users', [
'email' => $user->email,
]);
}
public function test_unverified_user_cannot_access_dashboard(): void
{
$user = AppModelsUser::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertRedirect('/email/verify');
}
}
“`
关键细节:在与数据库交互的测试类中,始终使用 `RefreshDatabase` 或 `DatabaseTransactions` trait。`RefreshDatabase` 在测试套件之前重新运行迁移,并将每个测试包装在事务中,事务在测试后回滚,保持测试隔离且幂等。
7.2 使用 `make()` 的单元测试
“`php
public function test_user_full_name_accessor(): void
{
$user = AppModelsUser::factory()->make([
'name' => 'Alice Wonderland',
]);
$this->assertEquals('Alice Wonderland', $user->name);
}
“`
不发生数据库交互。测试在微秒内运行,适合高频 CI 流水线。
第 8 步:使用工厂的数据库填充器
8.1 创建填充器
“`bash
php artisan make:seeder UserSeeder
“`
8.2 实现填充器
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use IlluminateDatabaseSeeder;
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(50)
->create();
}
}
“`
8.3 带关联关系的复合填充器
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use AppModelsPost;
use IlluminateDatabaseSeeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(20)
->has(Post::factory()->count(5), 'posts')
->create();
// Create 5 admin users with no posts
User::factory()->count(5)->admin()->create();
}
}
“`
8.4 运行填充器
“`bash
Run a specific seeder
php artisan db:seed –class=UserSeeder
Run all seeders defined in DatabaseSeeder
php artisan db:seed
Migrate fresh and seed in one command (common in staging resets)
php artisan migrate:fresh –seed
“`
`migrate:fresh –seed` 命令是将暂存或开发数据库重置为已知填充状态的标准工作流。在独立服务器上,此模式在 QA 周期之前经常使用,以确保干净、可重现的环境。
高级模式和边缘情况
延迟属性和依赖值
`definition()` 内的 Faker 值在每次工厂调用时重新求值。但是,如果您需要一个属性依赖于另一个属性,请使用闭包:
“`php
public function definition(): array
{
$firstName = $this->faker->firstName();
$lastName = $this->faker->lastName();
return [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => strtolower("{$firstName}.{$lastName}@example.com"),
'username' => strtolower("{$firstName}{$lastName}") . $this->faker->numerify('###'),
];
}
“`
这确保 `email` 和 `username` 从相同的名称值派生,生成内部一致的记录。
避免 `unique()` 溢出陷阱
当在单次工厂调用中生成大型数据集(10,000 条以上记录)时,`$this->faker->unique()->safeEmail()` 可能会耗尽 Faker 的唯一性池并抛出 `OverflowException`。通过在生成的值后附加 UUID 或时间戳来缓解此问题:
“`php
'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',
“`
这在大规模情况下保证唯一性,而无需依赖 Faker 的内部唯一性注册表。
工厂回调:`afterMaking` 和 `afterCreating`
使用回调执行无法表示为简单属性的创建后逻辑:
“`php
public function configure(): static
{
return $this->afterCreating(function (User $user) {
$user->profile()->create([
'bio' => $this->faker->paragraph(),
'avatar' => $this->faker->imageUrl(200, 200, 'people'),
]);
});
}
“`
`configure()` 方法在工厂实例化时调用一次。`afterCreating` 在模型持久化后运行,适合创建需要父模型主键的关联模型。
测试电子邮件功能
当工厂生成带有电子邮件地址的用户时,验证电子邮件发送的集成测试受益于配置了沙盒 SMTP 服务器的专用电子邮件托管环境,防止将测试电子邮件意外发送到真实地址。
`create()` 与 `make()` 与 `makeMany()` 与 `createMany()` — 比较
| 方法 | 持久化到数据库 | 返回值 | 最佳使用场景 |
|---|---|---|---|
| — | — | — | — |
| `create()` | 是 | 单个模型实例 | 功能测试、填充器 |
| `create(['key' => 'val'])` | 是 | 单个模型实例 | 需要特定已知值的测试 |
| `count(n)->create()` | 是 | n 个模型的集合 | 批量填充、分页测试 |
| `make()` | 否 | 单个模型实例 | 单元测试、访问器/修改器测试 |
| `make(['key' => 'val'])` | 否 | 单个模型实例 | 具有受控属性的快速单元测试 |
| `count(n)->make()` | 否 | n 个模型的集合 | 内存集合测试 |
| `createMany([…])` | 是 | 集合 | 具有不同属性集的批量创建 |
| `makeMany([…])` | 否 | 集合 | 批量内存实例 |
工厂状态与属性覆盖——何时使用各自
| 场景 | 推荐方法 |
|---|---|
| — | — |
| 可重用的领域条件(例如,”管理员用户”) | 命名状态方法 |
| 测试特定的一次性值 | 在 `create()` 中覆盖属性 |
| 循环遍历预定义集合 | `Sequence` |
| 创建后的副作用 | `afterCreating` 回调 |
| 依赖属性值 | `definition()` 中基于闭包的属性 |
| 关联关系填充 | `has()`、`for()`、`hasAttached()` |
实用决策清单
在编写工厂或使用工厂的测试之前,请检查以下要点:
- 测试是否依赖数据库?如果不是,请使用 `make()` 并避免 `RefreshDatabase` 开销。
- 测试是否需要特定属性值?将其作为覆盖值传递给 `create()`——不要在工厂 `definition()` 中硬编码它。
- 您是否在测试基于角色的行为?定义命名状态,而不是在多个测试文件中分散 `create(['is_admin' => true])`。
- 您是否在填充暂存环境?使用 `migrate:fresh –seed` 并确保您的 `DatabaseSeeder` 按正确的依赖顺序(父级在子级之前)组合所有子填充器。
- 您是否生成超过 5,000 条记录?避免在高基数字段上使用 `unique()`;改用 UUID 后缀值。
- 您的模型是否有访问外部服务的 `afterCreating` 回调?在测试设置中模拟这些服务,或使用 `make()` 完全绕过回调。
- 您是否并行运行测试?使用 `DatabaseTransactions` 而不是 `RefreshDatabase` 以避免并行工作进程之间的迁移冲突,或为每个工作进程配置单独的数据库连接。
对于跨环境管理多个 Laravel 应用程序的团队,VPS 控制面板提供了在大型填充操作和测试运行期间监控数据库性能所需的基础设施可见性。
常见问题
Laravel 工厂中 `create()` 和 `make()` 有什么区别?
`create()` 将模型持久化到数据库并返回一个已填充的 Eloquent 实例。`make()` 在内存中构建模型,不进行任何数据库交互。对于纯单元测试,使用 `make()` 以保持测试快速且隔离;当测试必须验证数据库状态时,使用 `create()`。
Laravel 工厂能处理多态关联关系吗?
可以。通过在工厂 `definition()` 中直接设置 `*_type` 和 `*_id` 多态列来定义 `morphTo` 关联关系,或在父模型持久化后使用 `afterCreating` 附加多态关联关系。没有内置的 `hasMorphedByMany()` 简写,因此显式属性设置是最可靠的方法。
如何防止工厂生成的电子邮件在测试期间被发送?
在您的 `.env.testing` 文件中设置 `MAIL_MAILER=array` 或 `MAIL_MAILER=log`。这会将所有邮件通过 Laravel 的数组或日志驱动程序路由,将消息捕获在内存中或写入日志文件,而不发送到 SMTP 服务器。然后您可以在测试中对 `Mail::assertSent()` 进行断言。
为什么 `faker->unique()->safeEmail()` 在大型数据集中抛出 `OverflowException`?
Faker 的 `unique()` 修饰符维护一个先前生成值的内存注册表。当结构有效的唯一值池耗尽时——这可能发生在数万条记录的情况下——它会抛出 `OverflowException`。解决方法是在基础电子邮件值后附加 UUID 或随机字符串,确保唯一性而不依赖 Faker 的注册表。
工厂应该用于生产填充器吗?
工厂是为开发和测试环境设计的。对于生产填充(例如,填充查找表、默认角色或配置记录),请使用具有硬编码、确定性值的专用填充器类。依赖 Faker 的工厂绝不应该在生产数据库上运行,因为它们会引入不可预测、不可审计的数据。
