Фабрики Laravel: Создание реалистичных тестовых данных с помощью моделей фабрик Laravel
При разработке приложений на Laravel одним из наиболее распространённых узких мест в процессе тестирования является генерация содержательных, реалистичных данных. Laravel factories — это классы, определяющие шаблон для создания экземпляров Eloquent-моделей с использованием PHP-библиотеки Faker для генерации случайных, но структурно корректных значений атрибутов, что позволяет разработчикам заполнять базы данных и писать изолированные тесты без ручного создания фикстур данных.
В отличие от статических SQL-файлов сидов или жёстко заданных массивов, factories являются компонуемыми, сохраняющими состояние и учитывающими связи. Они напрямую интегрируются с тестовыми наборами PHPUnit и Pest, поддерживают ленивое вычисление атрибутов и масштабируются от одного экземпляра модели до тысяч записей в одной цепочке методов. Если вы запускаете Laravel на VPS Хостинге, factories становятся особенно ценными при запусках CI/CD-пайплайнов, сбросах промежуточных сред и сценариях нагрузочного тестирования, где воспроизводимая и контролируемая генерация данных является обязательным условием.
Что такое Laravel Factories и почему они важны
Laravel factories были кардинально переработаны в Laravel 8. Старый подход на основе замыканий `$factory->define()` был заменён выделенными PHP-классами, расширяющими `IlluminateDatabaseEloquentFactoriesFactory`. Это архитектурное изменение обеспечило типобезопасность, автодополнение в IDE и более чёткое разделение между логикой factory и определениями моделей.
Каждый класс factory реализует метод `definition()`, возвращающий ассоциативный массив атрибутов модели. Factory автоматически разрешает экземпляр `FakerGenerator`, доступный через `$this->faker`, который поддерживает более 200 провайдеров данных с учётом локали — от `name()` и `safeEmail()` до `iban()`, `latitude()`, `uuid()` и `creditCardNumber()`.
Ключевые возможности современной системы factory:
- Цепочки методов для настройки количества, состояния и связей
- Ленивое вычисление атрибутов — замыкания внутри `definition()` вычисляются заново для каждого экземпляра
- Состояния factory для моделирования специфических для предметной области вариаций (например, заблокированные аккаунты, верифицированные пользователи)
- Связанные factories, рекурсивно создающие родительские модели при необходимости
- Последовательности для циклического перебора предопределённых наборов атрибутов
- `make()` против `create()` для экземпляров в памяти и сохранённых в базе данных
Предварительные требования
Перед реализацией factories убедитесь, что ваша среда соответствует следующим требованиям:
- Laravel 9 или новее (Laravel 8 совместим, но не поддерживает некоторые новые возможности последовательностей)
- PHP 8.0 или выше
- Настроенное подключение к базе данных в `.env` (MySQL, PostgreSQL или SQLite для тестирования в памяти)
- Пакет `laravel/framework`, поставляемый с `fakerphp/faker` в качестве зависимости
- Eloquent-модели с соответствующими файлами миграций
Для команд, запускающих Laravel на управляемой инфраструктуре, VPS с cPanel предоставляет удобную среду для управления как стеком приложения, так и службами базы данных через единый интерфейс.
Шаг 1: Создание класса Factory
Используйте Artisan CLI для создания файла factory:
“`bash
php artisan make:factory UserFactory
“`
Это создаёт `database/factories/UserFactory.php`. Если вы хотите автоматически связать factory с моделью, передайте флаг `–model`:
“`bash
php artisan make:factory UserFactory –model=User
“`
Laravel разрешает привязку factory к модели через соглашение об именовании: `UserFactory` соответствует `AppModelsUser`. Вы можете переопределить это, явно задав свойство `protected $model`, что необходимо, когда ваши модели находятся за пределами пространства имён `AppModels` по умолчанию.
Шаг 2: Определение шаблона Factory
Откройте `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` и возвращает гидратированную Eloquent-модель `User`. Возвращённый экземпляр отражает фактическое состояние базы данных, включая любые значения по умолчанию на уровне базы данных или триггеры.
3.2 Сохранение нескольких записей
“`php
$users = AppModelsUser::factory()->count(10)->create();
“`
Возвращает `IlluminateDatabaseEloquentCollection` из 10 экземпляров `User`. Каждая запись получает независимо сгенерированные значения 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: Состояния Factory
Состояния — это именованные модификации базового определения factory. Они позволяют моделировать различные условия предметной области без дублирования всего factory.
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: Связанные Factories
6.1 Определение связи Belongs-To
В `PostFactory.php` укажите родительский factory непосредственно в качестве значения атрибута:
“`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 откладывает его вычисление. Если вы вызываете `Post::factory()->create()` без указания `user_id`, автоматически создаётся новый `User` и используется его первичный ключ. Если вы предоставляете существующего пользователя, вложенный factory полностью пропускается.
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: Использование Factories в тестах
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` в тестовых классах, взаимодействующих с базой данных. `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: Сидеры базы данных с Factories
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 для обеспечения чистой, воспроизводимой среды.
Продвинутые шаблоны и граничные случаи
Ленивые атрибуты и зависимые значения
Значения Faker внутри `definition()` вычисляются заново при каждом вызове factory. Однако если вам нужно, чтобы один атрибут зависел от другого, используйте замыкание:
“`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+ записей в одном вызове factory) `$this->faker->unique()->safeEmail()` может исчерпать пул уникальности Faker и выбросить `OverflowException`. Смягчите это, добавив UUID или временную метку к сгенерированному значению:
“`php
'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',
“`
Это гарантирует уникальность при масштабировании без использования внутреннего реестра уникальности Faker.
Обратные вызовы Factory: `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()` вызывается один раз при создании экземпляра factory. `afterCreating` выполняется после сохранения модели, что делает его подходящим для создания связанных моделей, требующих первичного ключа родителя.
Тестирование функциональности электронной почты
Когда factories генерируют пользователей с адресами электронной почты, интеграционные тесты, проверяющие отправку писем, выигрывают от выделенной среды Email Хостинга, настроенной с sandbox SMTP-сервером, предотвращая случайную доставку тестовых писем на реальные адреса.
`create()` против `make()` против `makeMany()` против `createMany()` — Сравнение
| Метод | Сохраняет в БД | Возвращает | Лучший вариант использования |
|---|
| — | — | — | — |
|---|
| `create()` | Да | Один экземпляр модели | Функциональные тесты, сидеры |
|---|
| `create(['key' => 'val'])` | Да | Один экземпляр модели | Тесты, требующие конкретных известных значений |
|---|
| `count(n)->create()` | Да | Коллекция из n моделей | Массовое заполнение, тесты пагинации |
|---|
| `make()` | Нет | Один экземпляр модели | Модульные тесты, тестирование аксессоров/мутаторов |
|---|
| `make(['key' => 'val'])` | Нет | Один экземпляр модели | Быстрые модульные тесты с контролируемыми атрибутами |
|---|
| `count(n)->make()` | Нет | Коллекция из n моделей | Тестирование коллекций в памяти |
|---|
| `createMany([…])` | Да | Коллекция | Пакетное создание с различными наборами атрибутов |
|---|
| `makeMany([…])` | Нет | Коллекция | Пакетные экземпляры в памяти |
|---|
Состояния Factory против переопределений атрибутов — когда использовать каждый подход
| Сценарий | Рекомендуемый подход |
|---|
| — | — |
|---|
| Повторно используемое условие предметной области (например, «администратор») | Именованный метод состояния |
|---|
| Одноразовое значение для конкретного теста | Переопределение атрибута в `create()` |
|---|
| Циклический перебор предопределённого набора | `Sequence` |
|---|
| Побочные эффекты после создания | Обратный вызов `afterCreating` |
|---|
| Зависимые значения атрибутов | Атрибуты на основе замыканий в `definition()` |
|---|
| Заполнение связей | `has()`, `for()`, `hasAttached()` |
|---|
Практический контрольный список
Перед написанием factory или теста, использующего его, пройдите следующие контрольные точки:
- Зависит ли тест от базы данных? Если нет, используйте `make()` и избегайте накладных расходов `RefreshDatabase`.
- Требует ли тест конкретного значения атрибута? Передайте его как переопределение в `create()` — не задавайте его жёстко в `definition()` factory.
- Тестируете ли вы поведение на основе ролей? Определите именованные состояния вместо того, чтобы разбрасывать `create(['is_admin' => true])` по нескольким тестовым файлам.
- Заполняете ли вы промежуточную среду? Используйте `migrate:fresh –seed` и убедитесь, что ваш `DatabaseSeeder` компонует все вложенные сидеры в правильном порядке зависимостей (родители перед дочерними элементами).
- Генерируете ли вы более 5 000 записей? Избегайте `unique()` для полей с высокой кардинальностью; используйте значения с суффиксом UUID.
- Есть ли у ваших моделей обратные вызовы `afterCreating`, обращающиеся к внешним сервисам? Замокируйте эти сервисы в настройке теста или используйте `make()` для полного обхода обратного вызова.
- Запускаете ли вы тесты параллельно? Используйте `DatabaseTransactions` вместо `RefreshDatabase` для предотвращения конфликтов миграций между параллельными воркерами или настройте отдельные подключения к базе данных для каждого воркера.
Для команд, управляющих несколькими Laravel-приложениями в разных средах, Панели управления VPS обеспечивают необходимую видимость инфраструктуры для мониторинга производительности базы данных во время масштабных операций заполнения и тестовых запусков.
FAQ
В чём разница между `create()` и `make()` в Laravel factories?
`create()` сохраняет модель в базу данных и возвращает гидратированный Eloquent-экземпляр. `make()` создаёт модель в памяти без какого-либо взаимодействия с базой данных. Используйте `make()` для чистых модульных тестов, чтобы они оставались быстрыми и изолированными; используйте `create()`, когда тест должен проверять состояние базы данных.
Могут ли Laravel factories обрабатывать полиморфные связи?
Да. Определите связь `morphTo`, установив столбцы морфа `*_type` и `*_id` непосредственно в `definition()` factory, или используйте `afterCreating` для присоединения полиморфных связей после сохранения родительской модели. Встроенного сокращения `hasMorphedByMany()` не существует, поэтому явная установка атрибутов является наиболее надёжным подходом.
Как предотвратить отправку писем, сгенерированных factory, во время тестов?
Установите `MAIL_MAILER=array` или `MAIL_MAILER=log` в вашем файле `.env.testing`. Это направляет всю почту через драйвер array или log Laravel, сохраняя сообщения в памяти или записывая их в лог-файл без отправки на SMTP-сервер. Затем вы можете делать утверждения на `Mail::assertSent()` в ваших тестах.
Почему `faker->unique()->safeEmail()` выбрасывает `OverflowException` при больших наборах данных?
Модификатор `unique()` Faker поддерживает реестр ранее сгенерированных значений в памяти. Когда пул структурно корректных уникальных значений исчерпан — что может произойти при десятках тысяч записей — он выбрасывает `OverflowException`. Решение — добавить UUID или случайную строку к базовому значению email, обеспечивая уникальность без использования реестра Faker.
Следует ли использовать factories в производственных сидерах?
Factories предназначены для сред разработки и тестирования. Для производственного заполнения (например, заполнения справочных таблиц, ролей по умолчанию или записей конфигурации) используйте выделенные классы сидеров с жёстко заданными, детерминированными значениями. Factories, зависящие от Faker, никогда не должны запускаться против производственной базы данных, поскольку они вносят непредсказуемые, неаудируемые данные.
