15%

Збережіть 15% на всі хостинг-послуги

Перевірте свої навички і отримайте Знижку на будь-який план хостингу

Використовуй код:

Skills
Почати
09.10.2024

Laravel Factories: Створення реалістичних тестових даних за допомогою моделей Laravel Factory

При розробці додатків з 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:

  • Fluent-ланцюжок методів для налаштування кількості, стану та зв’язків
  • Ліниве обчислення атрибутів — замикання всередині `definition()` обчислюються заново для кожного екземпляра
  • Стани factory для моделювання специфічних для домену варіацій (наприклад, заблоковані акаунти, верифіковані користувачі)
  • Relationship factories, які рекурсивно створюють батьківські моделі за потреби
  • Sequences для циклічного перебору наперед визначених наборів атрибутів
  • `make()` проти `create()` для екземплярів у пам’яті проти збережених у базі даних

Передумови

Перед реалізацією factories переконайтеся, що ваше середовище відповідає таким вимогам:

  • Laravel 9 або новіший (Laravel 8 сумісний, але не має деяких новіших функцій sequence)
  • 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()` створює екземпляр моделі та заповнює її атрибути без звернення до бази даних. Це ідеально підходить для юніт-тестів, які перевіряють поведінку моделі, приведення атрибутів або логіку accessor/mutator в ізоляції — зберігаючи тести швидкими та незалежними від бази даних.

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: Sequences для циклічного перебору значень атрибутів

Коли вам потрібно чергувати між визначеним набором значень, а не випадковими, використовуйте `Sequence`:

“`php

use IlluminateDatabaseEloquentFactoriesSequence;

$users = AppModelsUser::factory()

->count(6)

->state(new Sequence(

['role' => 'editor'],

['role' => 'viewer'],

['role' => 'moderator'],

))

->create();

“`

Це циклічно перебирає масив sequence, призначаючи ролі по порядку. При 6 користувачах кожна роль призначається двічі. Sequences є незамінними для тестування пагінації, контролю доступу на основі ролей та логіки рендерингу UI, яка залежить від різноманітних, але контрольованих розподілів даних.

Крок 6: Relationship 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()` обробляє вставку в pivot-таблицю, включаючи будь-які додаткові атрибути pivot, які вам потрібно заповнити.

Крок 7: Використання Factories в тестах

7.1 Feature-тест з перевірками бази даних

“`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: Database Seeders з Factories

8.1 Створення Seeder

“`bash

php artisan make:seeder UserSeeder

“`

8.2 Реалізація Seeder

“`php

<?php

namespace DatabaseSeeders;

use AppModelsUser;

use IlluminateDatabaseSeeder;

class UserSeeder extends Seeder

{

public function run(): void

{

User::factory()

->count(50)

->create();

}

}

“`

8.3 Складений Seeder зі зв’язками

“`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 Запуск Seeder

“`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.

Callbacks Factory: `afterMaking` та `afterCreating`

Використовуйте callbacks для виконання пост-створювальної логіки, яку не можна виразити як простий атрибут:

“`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 Хостингу, налаштованого з пісочницею SMTP-сервера, що запобігає випадковій доставці тестових листів на реальні адреси.

`create()` проти `make()` проти `makeMany()` проти `createMany()` — Порівняння

МетодЗберігає в БДПовертаєНайкращий випадок використання
`create()`ТакОдин екземпляр моделіFeature-тести, seeders
`create(['key' => 'val'])`ТакОдин екземпляр моделіТести, що вимагають конкретних відомих значень
`count(n)->create()`ТакКолекція з n моделейМасове заповнення, тести пагінації
`make()`НіОдин екземпляр моделіЮніт-тести, тестування accessor/mutator
`make(['key' => 'val'])`НіОдин екземпляр моделіШвидкі юніт-тести з контрольованими атрибутами
`count(n)->make()`НіКолекція з n моделейТестування колекцій у пам’яті
`createMany([…])`ТакКолекціяПакетне створення з різними наборами атрибутів
`makeMany([…])`НіКолекціяПакетні екземпляри в пам’яті

Стани Factory проти перевизначень атрибутів — Коли використовувати кожен

СценарійРекомендований підхід
Повторно використовувана доменна умова (наприклад, “адміністратор”)Іменований метод стану
Одноразове значення для конкретного тестуПеревизначення атрибута в `create()`
Циклічний перебір наперед визначеного набору`Sequence`
Побічні ефекти після створенняCallback `afterCreating`
Залежні значення атрибутівАтрибути на основі замикань у `definition()`
Заповнення зв’язків`has()`, `for()`, `hasAttached()`

Практичний контрольний список рішень

Перед написанням factory або тесту, який його використовує, пройдіть ці контрольні точки:

  • Чи залежить тест від бази даних? Якщо ні, використовуйте `make()` та уникайте накладних витрат `RefreshDatabase`.
  • Чи вимагає тест конкретного значення атрибута? Передайте його як перевизначення до `create()` — не жорстко кодуйте його у `definition()` factory.
  • Чи тестуєте ви поведінку на основі ролей? Визначте іменовані стани, а не розкидайте `create(['is_admin' => true])` по кількох тестових файлах.
  • Чи заповнюєте ви стейджинг-середовище? Використовуйте `migrate:fresh –seed` та переконайтеся, що ваш `DatabaseSeeder` компонує всі під-seeders у правильному порядку залежностей (батьки перед дітьми).
  • Чи генеруєте ви більше 5 000 записів? Уникайте `unique()` на полях з високою кардинальністю; натомість використовуйте значення з суфіксом UUID.
  • Чи мають ваші моделі callbacks `afterCreating`, які звертаються до зовнішніх сервісів? Замокайте ці сервіси у налаштуванні тесту або використовуйте `make()` для повного обходу callback.
  • Чи запускаєте ви тести паралельно? Використовуйте `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`. Це направляє всю пошту через масивний або log-драйвер Laravel, захоплюючи повідомлення в пам’яті або записуючи їх у лог-файл без відправки на SMTP-сервер. Потім ви можете робити перевірки на `Mail::assertSent()` у ваших тестах.

Чому `faker->unique()->safeEmail()` кидає `OverflowException` у великих наборах даних?

Модифікатор `unique()` Faker підтримує реєстр раніше згенерованих значень у пам’яті. Коли пул структурно валідних унікальних значень вичерпується — що може статися з десятками тисяч записів — він кидає `OverflowException`. Виправлення полягає в додаванні UUID або випадкового рядка до базового значення електронної пошти, забезпечуючи унікальність без покладання на реєстр Faker.

Чи слід використовувати factories у production seeders?

Factories призначені для середовищ розробки та тестування. Для production-заповнення (наприклад, заповнення таблиць пошуку, ролей за замовчуванням або записів конфігурації) використовуйте виділені класи seeder з жорстко закодованими, детермінованими значеннями. Factories, що залежать від Faker, ніколи не повинні запускатися проти production-бази даних, оскільки вони вводять непередбачувані, неаудитовані дані.

15%

Збережіть 15% на всі хостинг-послуги

Перевірте свої навички і отримайте Знижку на будь-який план хостингу

Використовуй код:

Skills
Почати