Дані для тесту відповідають на питання читача: чому тест очікує саме цей результат? Якщо вакансії, компанії й користувачі лежать у спільному дампі, відповідь треба шукати в 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);
});
Скажіть правило одним реченням: «у тесті видно лише ті поля, від яких залежить перевірка, решту бере фабрика чи builder». Потім покажіть стан фабрики з бізнес-назвою (`->expired()`) і пастку зі спільними фікстурами. Інтервʼюер чує, що ви думаєте про читача тесту, а не про те, як швидше заповнити базу.