Тесты в Django: фикстуры, фабрики и клиент без боли
Три инструмента подготовки данных в тестах Django — JSON-фикстуры, factory_boy и ручная сборка — дают разный результат по скорости и хрупкости. Разбираем, что когда выбрать и где каждый ломается.

Тест, который готовит данные через User.objects.create() в каждом методе, растёт вместе с проектом: сначала три строки на подготовку, потом двадцать, а через год половина файла — это setup, а не проверки. Django предлагает несколько способов готовить данные для тестов, и они не взаимозаменяемы: JSON-фикстуры удобны для справочников, но хрупки; factory_boy гибок, но добавляет зависимость; ручная сборка прозрачна, но многословна. Ниже — как эти три подхода ведут себя на реальном тестовом наборе и по какому критерию выбирать. Актуально для Django 5.x и pytest-django 4.x.
Задача: подготовить данные, не утонув в setup
Возьмём типичный случай: тестируем вью, которая показывает заказы пользователя. Чтобы проверить одну строку в шаблоне, нужен пользователь, магазин, пара товаров и заказ, связывающий всё это. Очевидное решение — создать объекты прямо в тесте:
# Django 5.x, стандартный TestCase
from django.test import TestCase
from shop.models import Order
class OrderViewTest(TestCase):
def test_shows_user_orders(self):
user = User.objects.create_user("anna", password="pass12345")
shop = Shop.objects.create(name="Книги")
book = Product.objects.create(shop=shop, title="Django", price=990)
order = Order.objects.create(user=user, shop=shop)
order.items.add(book)
# только теперь начинается сам тест
self.client.force_login(user)
response = self.client.get("/orders/")
self.assertContains(response, "Django")
Пять строк подготовки на одну проверку. Когда таких тестов станет тридцать, изменение в конструкторе Product заставит править тридцать мест. Отсюда и растут три альтернативы.
Подход 1. JSON-фикстуры и loaddata
Django умеет загружать данные из файла через loaddata. Сериализуете текущее состояние базы командой dumpdata, кладёте результат в fixtures/ и подключаете в тесте атрибутом fixtures.
class OrderViewTest(TestCase):
fixtures = ["orders_sample.json"]
def test_shows_user_orders(self):
self.client.force_login(User.objects.get(username="anna"))
response = self.client.get("/orders/")
self.assertContains(response, "Django")
Файл фикстуры — это массив объектов с полями model, pk и fields. Формат жёсткий: одна опечатка в имени поля — и loaddata падает с DeserializationError.
Фикстура фиксирует не данные, а схему на момент экспорта. Добавили обязательное поле в модель — все старые JSON-фикстуры без этого поля мгновенно перестают грузиться.
Отсюда главная беда фикстур: они хрупкие. Каждая миграция, меняющая структуру таблицы, рискует сломать десяток файлов, которые никто не открывал полгода. Поэтому JSON-фикстуры хороши там, где данные стабильны: справочники валют, список регионов, начальные роли. Для бизнес-объектов, которые меняются вместе с моделями, — плохой выбор.
Подход 2. factory_boy
Библиотека factory_boy описывает, как собрать объект, а не какими данными его заполнить. Вы задаёте фабрику один раз, а в тестах просите ровно те поля, которые важны для конкретной проверки. Остальное фабрика придумает сама.
# pip install factory_boy
# factory_boy 3.x
import factory
from shop.models import Shop, Product, Order
class ShopFactory(factory.django.DjangoModelFactory):
class Meta:
model = Shop
name = factory.Sequence(lambda n: f"Магазин {n}")
class ProductFactory(factory.django.DjangoModelFactory):
class Meta:
model = Product
shop = factory.SubFactory(ShopFactory)
title = factory.Faker("sentence", nb_words=2, locale="ru_RU")
price = 990
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
user = factory.SubFactory("tests.factories.UserFactory")
shop = factory.SubFactory(ShopFactory)
Теперь тест сообщает только то, что важно для проверки, — а связанные объекты соберутся через SubFactory автоматически:
class OrderViewTest(TestCase):
def test_shows_user_orders(self):
product = ProductFactory(title="Django")
order = OrderFactory(shop=product.shop)
order.items.add(product)
self.client.force_login(order.user)
response = self.client.get("/orders/")
self.assertContains(response, "Django")
Плюс в устойчивости к изменениям: добавили обязательное поле в Product — правите одну фабрику, а не тридцать тестов. Минус — фабрика скрывает, какие данные реально попадают в базу. Когда тест падает, приходится читать определение фабрики, чтобы понять, что за title сгенерировал Faker. И ещё: Faker даёт случайные значения, поэтому не проверяйте в assert'ах то, что фабрика сгенерировала сама — только то, что вы задали явно.
build против create
Вызов ProductFactory() пишет объект в базу. Если объект в базе не нужен — например, тестируете чистую логику метода модели без запросов, — берите ProductFactory.build(): он вернёт несохранённый экземпляр и не тронет БД. На больших наборах разница по времени заметная: каждый лишний INSERT стоит времени.
Подход 3. Ручная сборка в setUp или фикстурах pytest
Третий путь — не тащить зависимость и собирать объекты руками, но вынести это из тела теста. В классическом TestCase — в setUpTestData, в pytest — в фикстуру.
# pytest-django 4.x
import pytest
from shop.models import Shop, Product
@pytest.fixture
def product_with_shop(db):
shop = Shop.objects.create(name="Книги")
return Product.objects.create(shop=shop, title="Django", price=990)
def test_shows_user_orders(client, django_user_model, product_with_shop):
user = django_user_model.objects.create_user("anna", password="pass12345")
client.force_login(user)
response = client.get("/orders/")
assert "Django" in response.content.decode()
Прозрачно: видно ровно то, что создаётся. Аргумент db в фикстуре — маркер pytest-django, дающий доступ к базе. Минус — многословность и дублирование, когда наборов данных много и они пересекаются.
setUpTestData вместо setUp
Если пишете на TestCase, готовьте общие данные в setUpTestData, а не в setUp. Первый вызывается один раз на класс и оборачивает данные в транзакцию, откатываемую после класса; второй — перед каждым методом. На классе из двадцати тестов это разница в двадцать наборов INSERT против одного.
Что выбирать
| Критерий | JSON-фикстуры | factory_boy | Ручная сборка |
|---|---|---|---|
| Устойчивость к миграциям | Низкая | Высокая | Высокая |
| Прозрачность данных в тесте | Средняя | Низкая | Высокая |
| Многословность | Низкая | Низкая | Высокая |
| Внешняя зависимость | Нет | Да | Нет |
| Лучше всего для | Стабильные справочники | Бизнес-объекты со связями | Единичные простые случаи |
- Справочники и seed-данные, которые почти не меняются, — JSON-фикстуры. Их редко трогают, зато грузятся быстро и без кода.
- Связанные бизнес-объекты (заказ → магазин → товар), которых в тестах много, — factory_boy. Экономия на поддержке окупает зависимость.
- Пара простых объектов в одном-двух тестах — ручная сборка. Тащить фабрику ради двух вызовов незачем.
Тестовый клиент: где чаще спотыкаются
Клиент Django (self.client или фикстура client в pytest) — отдельный источник ошибок независимо от способа подготовки данных.
- force_login против login.
login()проверяет пароль и работает через бэкенд аутентификации — это лишняя работа. Для тестов, где важен только факт «пользователь вошёл», беритеforce_login(user): он ставит сессию напрямую, минуя проверку пароля. - follow=True для редиректов. После POST вью часто делает redirect. Без
follow=Trueв ответе будет код 302 и пустое тело, аassertContainsпроверит не ту страницу. Сfollow=Trueклиент пройдёт по цепочке и вернёт финальный ответ. - reverse вместо строк. Хардкод
"/orders/"ломается при смене URL. Используйтеreverse("orders:list")— тест не развалится от косметической правки маршрута. - Изоляция транзакций. Обычный
TestCaseоборачивает каждый тест в транзакцию и откатывает её. Если тестируете код, который сам управляет транзакциями или проверяет коммиты, нуженTransactionTestCase— иначе поведение в тесте не совпадёт с продом.
Что ломается на практике
- Случайные значения в assert.
Fakerдаёт разный результат при каждом запуске. Проверяетеtitle— задайте его явно в вызове фабрики, а не полагайтесь на сгенерированное. - Фикстуры и автоинкремент pk. Жёстко прописанные
pkв JSON конфликтуют с последовательностями базы, если тесты создают другие объекты. Симптом —IntegrityError: duplicate key. - SubFactory создаёт лишнее. Каждый
SubFactoryбез явного значения порождает новый объект. Указали заказу и товару разные магазины по недосмотру — тест проверяет несуществующую в бизнес-логике ситуацию. - build() и обращение к связям. Несохранённый через
build()объект не имеет pk, поэтому обращение к обратным связям (order.items.all()) упадёт. Для проверок со связями нуженcreate().
Что делать дальше
Соберите фабрики в один модуль tests/factories.py и подключите pytest-factoryboy, если работаете на pytest, — он превращает фабрики в фикстуры автоматически. Замерьте время прогона до и после переноса подготовки данных в setUpTestData: на больших наборах это часто главный резерв скорости. И проверьте, не сериализует ли ваш прогон базу каждый раз — параметр --reuse-db в pytest-django переиспользует тестовую БД между запусками и экономит секунды на каждой миграции.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Django documentation: Testing tools, factory_boy documentation
Частые вопросы
У меня другая версия Django или factory_boy, примеры сработают?
Тест падает с DeserializationError при загрузке фикстуры — что делать?
Что быстрее — фикстуры, фабрики или ручная сборка?
Можно ли смешивать фикстуры и фабрики в одном проекте?
force_login не срабатывает, вью всё равно редиректит на логин — почему?
Нужен ли pytest, или хватит стандартного TestCase?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.