<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
ТЕСТУВАННЯ · MIDDLE

Як готувати дані для тестів: фабрики, фікстури чи builders?

Кожен тест має сам створювати рівно ті дані, від яких залежить його перевірка, і показувати їх у тексті тесту. Для моделей це фабрики зі станами (`Vacancy::factory()->published()`), для доменних обʼєктів у unit-тестах builder або Object Mother з розумними значеннями за замовчуванням. Спільні фікстури на весь набір зручні лише для довідників, бо інакше тести неявно залежать від чужих даних і ламаються від кожної зміни.

Чим фабрики моделей кращі за спільний SQL-дамп з тестовими даними?
Тест падає, бо хтось змінив рядок у спільних фікстурах. Як цього уникнути?
Один `Vacancy::factory()->create()` створює в базі дванадцять рядків. Чому і чи це проблема?
Як готувати складні доменні обʼєкти в unit-тестах без бази?
фабрики фікстури test data builder Object Mother Foundry

Дані для тесту відповідають на питання читача: чому тест очікує саме цей результат? Якщо вакансії, компанії й користувачі лежать у спільному дампі, відповідь треба шукати в SQL-файлі на дві тисячі рядків, а тест з toBe(37) ламається, щойно хтось додасть 38-му вакансію для свого сценарію. Джерард Месарош (Gerard Meszaros) описав це як shared fixture і mystery guest: тест залежить від даних, яких у ньому не видно. Тому основне правило просте: кожен тест сам створює те, від чого залежить його перевірка, і показує це в тексті.

У Laravel для цього є фабрики моделей. Стани з бізнес-назвами (->published(), ->expired()) описують ситуацію, а не набір полів, і зміна правила («застаріла» тепер через 45 днів) правиться в одному місці. make() будує модель без запису в базу, create() зберігає. for() і has() задають звʼязки, sequence() чергує значення між моделями, а recycle() передає вже створену модель усім вкладеним фабрикам, щоб три вакансії справді належали одній компанії. У Symfony ту саму роль грає Foundry (zenstruck/foundry), а класичні Doctrine-фікстури частіше лишають для демо-даних.

Решту полів заповнюють розумні значення за замовчуванням, і тут є пастка: випадкові дані з Faker допустимі лише там, де вони не впливають на результат. Зарплата, згенерована fake()->numberBetween(), у тесті на фільтр за зарплатою робить його flaky. Значення, від яких залежить асерт, задають явно.

Для доменних обʼєктів у unit-тестах база не потрібна, і замість фабрик моделей беруть Test Data Builder (Нат Прайс) або Object Mother. Builder тримає валідні значення за замовчуванням і дає ланцюжок with...(), тож тест змінює лише те, що перевіряє: aVacancy()->withSalary(5000, 7000)->build(). Коли конструктор доменного класу отримує новий параметр, правити треба один builder, а не сто тестів.

Спільні дані мають своє законне місце: незмінні довідники на кшталт валют, країн і ролей, які потрібні всім і які жоден тест не змінює. Їх засівають один раз. А от вкладені фабрики варто тримати під контролем: кожна вакансія, що тягне нову компанію, нового власника й нове місто, додає INSERT-и, хешування паролів і мілісекунди, які на тисячі тестів складаються у хвилини.

// database/factories/VacancyFactory.php: стан з бізнес-назвою замість полів у тесті.
public function expired(): static
{
    return $this->state(fn () => ['published_at' => now()->subDays(61)]);
}

// Feature-тест: у тексті видно лише те, від чого залежить перевірка.
it('lists only fresh vacancies of a company', function () {
    $company = Company::factory()->create();

    Vacancy::factory()->recycle($company)->create(['title' => 'Senior Laravel']);
    Vacancy::factory()->recycle($company)->expired()->create(['title' => 'Old PHP']);
    Vacancy::factory()->create(['title' => 'Чужа вакансія']);   // інша компанія

    $this->get("/companies/{$company->slug}")
        ->assertSee('Senior Laravel')
        ->assertDontSee('Old PHP')
        ->assertDontSee('Чужа вакансія');
});

// Unit-тест домену: builder зі значеннями за замовчуванням, без бази.
it('computes the salary midpoint', function () {
    $vacancy = aVacancy()->withSalary(5000, 7000)->build();

    expect($vacancy->salary->midpoint())->toBe(6000);
});
Принцип: дані, від яких залежить асерт, видно в самому тесті; решту заповнюють розумні значення за замовчуванням.
Проблема спільних фікстур (у Месароша «mystery guest» і shared fixture): тести неявно залежать від даних, які хтось інший може змінити, і неясно, чому тест очікує саме 3.
Можливості фабрик Laravel: стани, `make()` проти `create()`, `for()`/`has()`, `sequence()`, `recycle()` для однієї спільної повʼязаної моделі; у Symfony аналог Foundry.
Test Data Builder (Нат Прайс) і Object Mother для доменних обʼєктів без бази: `aVacancy()->withSalary(5000, 7000)->build()`.
Ціна вкладених фабрик: каскад повʼязаних моделей сповільнює набір і ховає, що тест насправді використовує.
Тримати один великий сід або дамп на весь набір і писати `expect(Vacancy::count())->toBe(37)`: будь-який новий рядок ламає десятки тестів.
Покладатися на випадкові значення фабрики там, де від них залежить перевірка: сьогодні `fake()->numberBetween()` дав 6000, завтра 3000, і тест на фільтр зарплати падає раз на тиждень.
Викликати `create()` там, де вистачає `make()`: тест на метод моделі чи серіалізацію пише в базу без потреби.
Створювати повʼязані моделі вкладеними фабриками без `recycle()`: кожна вакансія тягне нову компанію, нового користувача й нове місто, і тест на «вакансії однієї компанії» перевіряє не те.
Описувати стан полями в кожному тесті (`['status' => 'published', 'published_at' => now(), 'closed_at' => null]`) замість стану фабрики з бізнес-назвою.
ПОРАДА

Скажіть правило одним реченням: «у тесті видно лише ті поля, від яких залежить перевірка, решту бере фабрика чи builder». Потім покажіть стан фабрики з бізнес-назвою (`->expired()`) і пастку зі спільними фікстурами. Інтервʼюер чує, що ви думаєте про читача тесту, а не про те, як швидше заповнити базу.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Спільні дані роблять тести залежними одне від одного і незрозумілими: чому очікуємо саме 37? Випадкові значення в полях, від яких залежить перевірка, дають flaky-тести. make() будує модель без запису в базу, create() зберігає. Правильний підхід: явні дані сценарію в тесті й фабрика або builder для решти.