Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Тестовые данные: фабрики вместо жёстких фикстур

Тестовые данные: фабрики объектов против фикстур с хардкодом

Фабрики тестовых данных: factory_boy и Faker, переопределение полей, связанные объекты, стратегии build/create и порядок в тестах.

Прямой ответ

Фабрика тестовых данных — объект, который создаёт экземпляр сущности с разумными значениями по умолчанию, позволяя переопределить только то, что важно для конкретного теста. Это альтернатива большим фикстурам с хардкодом, где каждый тест наследует состояние, которое ему не нужно.

Проблема, которую решают фабрики:

  1. Хрупкость. Фикстура с двадцатью полями ломается при изменении модели, даже если тесту важны два поля.
  2. Непонятность. Читая тест, нельзя понять, какие данные существенны: всё задаётся где-то в другом месте.
  3. Дублирование. Похожие фикстуры копируют друг друга с небольшими отличиями.
  4. Связанность. Тесты начинают зависеть от порядка создания и общего состояния.

В 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.

Рекомендации:

  1. Минимум по умолчанию, максимум явного в тесте. Фабрика задаёт то, что не важно; тест указывает то, что важно.
  2. Избегайте глубоких цепочек SubFactory. Создание заказа тянет клиента, клиент тянет страну, страна тянет валюту. Такие цепочки замедляют тесты и делают падения неочевидными.
  3. Держите фабрики в одном месте. Каталог tests/factories/ с одной фабрикой на модель; разнесённые по тестам копии расходятся.
  4. Отключайте внешние вызовы при создании. Если сохранение модели отправляет письмо или пишет в очередь, в тестах это подменяют — см. тестовые двойники.
  5. Проверяйте скорость. Фабрики, создающие по пять связанных объектов на тест, превращают набор из тысячи тестов в десятиминутный прогон.

Практический сценарий: переход от фикстур к фабрикам

Контекст: в проекте было 40 фикстур в conftest.py, самый большой тестовый файл содержал 90 строк подготовки данных. Добавление поля в модель ломало в среднем шесть фикстур.

  1. Диагностика. Разбор показал: 28 фикстур отличались друг от друга одним-двумя полями, а 11 полей в них не использовались ни одним тестом.
  2. Шаг 1. Создали по одной фабрике на модель с минимальным набором обязательных полей; уникальные поля через Sequence, правдоподобные — через Faker.
  3. Шаг 2. Тесты перевели на явное указание значимых полей. Фикстуры оставили только там, где нужен контекст: клиент с аутентификацией, транзакция, временный каталог.
  4. Шаг 3. Часто повторяющиеся комбинации оформили трейтами (paid=True, archived=True) вместо отдельных фабрик.
  5. Результат. Число фикстур сократилось с 40 до 9, средний тест потерял 30 строк подготовки, добавление нового поля в модель перестало требовать правок в тестах. Время прогона снизилось на 18 % за счёт уменьшения числа создаваемых связанных объектов.

Вывод: фабрики не сокращают объём кода сами по себе — они перемещают его туда, где он не мешает. Главный выигрыш в том, что тест начинает показывать только то, что действительно проверяет.

FAQ

Можно ли обойтись без factory_boy, используя только фикстуры pytest? Да, но с ростом числа моделей фикстуры начинают дублировать друг друга, а переопределение отдельных полей требует параметризации фикстур, что читается хуже. Фабрики решают именно задачу вариативного создания объектов.

Нужен ли Faker, если данные могут быть любыми? Faker полезен там, где важна правдоподобность: проверка валидации имени, адреса, телефона, а также для обнаружения проблем с кодировкой и длиной. Для чисто числовых расчётов достаточно детерминированных значений.

Не делает ли случайность тесты нестабильными? Делает, если случайные значения попадают в проверяемые поля или в поля с ограничениями. Правило: уникальность — через Sequence, проверяемые значения — явно, всё остальное — по умолчанию.

Что использовать для данных в интеграционных тестах с реальной БД? Те же фабрики со стратегией create(), но внутри транзакции, которая откатывается после теста. Это сохраняет изоляцию и скорость — см. интеграционные тесты в Docker.

Источники: factory_boy docs, Faker docs, pytest docs (fixtures) — ссылки в шапке.