15%

Сэкономьте 15% на всех хостинговых услугах

Проверьте свои навыки и получите скидку на любой тарифный план

Используйте код:

Skills
Начать
09.10.2024

Фабрики 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, никогда не должны запускаться против производственной базы данных, поскольку они вносят непредсказуемые, неаудируемые данные.

15%

Сэкономьте 15% на всех хостинговых услугах

Проверьте свои навыки и получите скидку на любой тарифный план

Используйте код:

Skills
Начать