Прямой ответ
Фабрика тестовых данных — объект, который создаёт экземпляр сущности с разумными значениями по умолчанию, позволяя переопределить только то, что важно для конкретного теста. Это альтернатива большим фикстурам с хардкодом, где каждый тест наследует состояние, которое ему не нужно.
Проблема, которую решают фабрики:
- Хрупкость. Фикстура с двадцатью полями ломается при изменении модели, даже если тесту важны два поля.
- Непонятность. Читая тест, нельзя понять, какие данные существенны: всё задаётся где-то в другом месте.
- Дублирование. Похожие фикстуры копируют друг друга с небольшими отличиями.
- Связанность. Тесты начинают зависеть от порядка создания и общего состояния.
В Python стандартный инструмент — factory_boy вместе с Faker для правдоподобных значений:
import factory
from faker import Faker
fake = Faker("ru_RU")
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = "accounts.User"
email = factory.Sequence(lambda n: f"user{n}@example.test")
full_name = factory.LazyAttribute(lambda _: fake.name())
is_active = True
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = "orders.Order"
customer = factory.SubFactory(UserFactory) # связанный объект создаётся автоматически
status = "new"
total = factory.LazyFunction(lambda: round(fake.pyfloat(2, positive=True), 2))
Использование в тесте показывает только существенное:
def test_refund_requires_paid_status(order_factory):
order = order_factory(status="paid", total=500) # важно только это
assert refund(order).is_ok()
Стратегии создания:
| Метод | Что делает | Когда применять |
|---|---|---|
build() |
Создаёт объект без записи в БД | Проверка логики без хранилища |
create() |
Создаёт и сохраняет | Нужны связи и запросы |
build_batch(n) / create_batch(n) |
Пакет объектов | Проверка списков и агрегатов |
attributes() |
Только словарь значений | Проверка сериализации, payloads |
Практика: приёмы и ограничения
1. Sequence для уникальных полей. Почта, логин, артикул — всё, что имеет ограничение уникальности. Случайные значения дают редкие, но болезненные коллизии, которые проявляются как нестабильные тесты — см. flaky-тесты.
2. LazyAttribute и LazyFunction для зависимых полей. Значение вычисляется в момент создания, а не при импорте модуля. Это важно: значение, вычисленное один раз при импорте, станет общим для всех тестов.
3. SubFactory для связей, SelfAttribute — для ссылок на себя.
class PaymentFactory(factory.django.DjangoModelFactory):
class Meta:
model = "orders.Payment"
order = factory.SubFactory(OrderFactory)
amount = factory.SelfAttribute("order.total") # сумма совпадает с суммой заказа
currency = factory.SelfAttribute("order.customer.country.currency")
4. Трейты и параметры — вместо десятка фабрик.
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = "orders.Order"
class Params:
refunded = factory.Trait(status="refunded", refunded_at=factory.LazyFunction(fake.date_time_this_year))
order = OrderFactory.create(refunded=True)
5. Не используйте фабрики для данных, которые тестируете. Если тест проверяет расчёт скидки, значения должны быть заданы явно: total=100, discount=0.1. Случайные значения из Faker в проверяемых полях делают тест нечитаемым и нестабильным.
6. Разделяйте фабрики и фикстуры. Фабрика отвечает за создание объекта, фикстура pytest — за жизненный цикл и контекст (транзакция, клиент, окружение). О фикстурах — в статье про pytest.
Рекомендации:
- Минимум по умолчанию, максимум явного в тесте. Фабрика задаёт то, что не важно; тест указывает то, что важно.
- Избегайте глубоких цепочек
SubFactory. Создание заказа тянет клиента, клиент тянет страну, страна тянет валюту. Такие цепочки замедляют тесты и делают падения неочевидными. - Держите фабрики в одном месте. Каталог
tests/factories/с одной фабрикой на модель; разнесённые по тестам копии расходятся. - Отключайте внешние вызовы при создании. Если сохранение модели отправляет письмо или пишет в очередь, в тестах это подменяют — см. тестовые двойники.
- Проверяйте скорость. Фабрики, создающие по пять связанных объектов на тест, превращают набор из тысячи тестов в десятиминутный прогон.
Практический сценарий: переход от фикстур к фабрикам
Контекст: в проекте было 40 фикстур в conftest.py, самый большой тестовый файл содержал 90 строк подготовки данных. Добавление поля в модель ломало в среднем шесть фикстур.
- Диагностика. Разбор показал: 28 фикстур отличались друг от друга одним-двумя полями, а 11 полей в них не использовались ни одним тестом.
- Шаг 1. Создали по одной фабрике на модель с минимальным набором обязательных полей; уникальные поля через
Sequence, правдоподобные — через Faker. - Шаг 2. Тесты перевели на явное указание значимых полей. Фикстуры оставили только там, где нужен контекст: клиент с аутентификацией, транзакция, временный каталог.
- Шаг 3. Часто повторяющиеся комбинации оформили трейтами (
paid=True,archived=True) вместо отдельных фабрик. - Результат. Число фикстур сократилось с 40 до 9, средний тест потерял 30 строк подготовки, добавление нового поля в модель перестало требовать правок в тестах. Время прогона снизилось на 18 % за счёт уменьшения числа создаваемых связанных объектов.
Вывод: фабрики не сокращают объём кода сами по себе — они перемещают его туда, где он не мешает. Главный выигрыш в том, что тест начинает показывать только то, что действительно проверяет.
FAQ
Можно ли обойтись без factory_boy, используя только фикстуры pytest? Да, но с ростом числа моделей фикстуры начинают дублировать друг друга, а переопределение отдельных полей требует параметризации фикстур, что читается хуже. Фабрики решают именно задачу вариативного создания объектов.
Нужен ли Faker, если данные могут быть любыми? Faker полезен там, где важна правдоподобность: проверка валидации имени, адреса, телефона, а также для обнаружения проблем с кодировкой и длиной. Для чисто числовых расчётов достаточно детерминированных значений.
Не делает ли случайность тесты нестабильными?
Делает, если случайные значения попадают в проверяемые поля или в поля с ограничениями. Правило: уникальность — через Sequence, проверяемые значения — явно, всё остальное — по умолчанию.
Что использовать для данных в интеграционных тестах с реальной БД?
Те же фабрики со стратегией create(), но внутри транзакции, которая откатывается после теста. Это сохраняет изоляцию и скорость — см. интеграционные тесты в Docker.
Источники: factory_boy docs, Faker docs, pytest docs (fixtures) — ссылки в шапке.