Создаём фабрики с factory_boy для моделей БД. Генерируем реалистичные данные с faker. Зависимые фабрики для FK relationships. Переиспользование в тестах. Избавляемся от ручного создания данных
В production-тестах вы создаёте СОТНИ тестовых записей: пользователи, заказы, платежи. Ручное создание превращается в ад копипасты.
factory_boy — индустриальный стандарт для генерации тестовых данных в Python.
Цель: Научиться создавать фабрики для моделей БД, генерировать реалистичные данные и переиспользовать их во всех тестах.
Вы должны уметь:
# Работать с PostgreSQL в pytest
@pytest.fixture
def db_connection():
conn = psycopg2.connect(...)
yield conn
def test_example(db_connection):
cur = db_connection.cursor()
cur.execute("INSERT INTO users ...")Если не уверены — вернитесь к Alembic миграции.
# tests/test_orders.py
def test_create_order(db_connection):
"""Тест создания заказа"""
cur = db_connection.cursor()
# ❌ Создаём пользователя вручную
cur.execute(
"INSERT INTO users (email, name) VALUES (%s, %s) RETURNING id",
("test@test.com", "Test User")
)
user_id = cur.fetchone()[0]
# ❌ Создаём продукт вручную
cur.execute(
"INSERT INTO products (title, price) VALUES (%s, %s) RETURNING id",
("Test Product", 100.0)
)
product_id = cur.fetchone()[0]
# ❌ Создаём заказ вручную
cur.execute(
"INSERT INTO orders (user_id, product_id, quantity) VALUES (%s, %s, %s) RETURNING id",
(user_id, product_id, 2)
)
order_id = cur.fetchone()[0]
# Проверка
assert order_id is not NoneПроблемы:
# С фабриками
def test_create_order(db_connection):
"""Тест создания заказа"""
order = OrderFactory(quantity=2)
assert order.id is not None
assert order.quantity == 2
# ✅ user и product созданы автоматически!✅ 90% меньше кода!
pip install factory_boy fakerfactory_boy — создание фабрик faker — генерация реалистичных данных (имена, email, адреса)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);# tests/factories.py
import factory
from faker import Faker
fake = Faker()
class UserFactory(factory.Factory):
"""Фабрика для создания пользователей"""
class Meta:
model = dict # Генерируем dict (для SQL INSERT)
email = factory.LazyFunction(lambda: fake.email())
name = factory.LazyFunction(lambda: fake.name())
created_at = factory.LazyFunction(lambda: fake.date_time())Что здесь происходит:
factory.Factory — базовый класс фабрикиmodel = dict — генерируем словарь (для psycopg2)LazyFunction — вызывает функцию каждый раз (не один раз при импорте!)fake.email() — генерирует реалистичный email: alice@example.comfake.name() — генерирует имя: Alice Johnson# tests/test_users.py
from factories import UserFactory
def test_create_user(db_connection):
"""Тест создания пользователя с фабрикой"""
cur = db_connection.cursor()
# Создайте пользователя через фабрику
user_data = UserFactory()
# Вставьте в БД
cur.execute(
"INSERT INTO users (email, name) VALUES (%(email)s, %(name)s) RETURNING id",
user_data
)
user_id = cur.fetchone()[0]
# Проверка
cur.execute("SELECT email FROM users WHERE id = %s", (user_id,))
result = cur.fetchone()
assert result[0] == user_data["email"]def test_bulk_users(db_connection):
"""Создайте 10 пользователей за раз"""
cur = db_connection.cursor()
# Создайте 10 пользователей
users = [UserFactory() for _ in range(10)]
for user in users:
cur.execute(
"INSERT INTO users (email, name) VALUES (%(email)s, %(name)s)",
user
)
# Проверка
cur.execute("SELECT COUNT(*) FROM users")
count = cur.fetchone()[0]
assert count == 10from faker import Faker
fake = Faker()
# Личные данные
fake.name() # "Alice Johnson"
fake.first_name() # "Alice"
fake.last_name() # "Johnson"
fake.email() # "alice@example.com"
fake.phone_number() # "+1-555-123-4567"
# Адреса
fake.address() # "123 Main St, Springfield, IL 62701"
fake.city() # "Springfield"
fake.country() # "United States"
# Даты
fake.date() # "2025-01-15"
fake.date_time() # datetime(2025, 1, 15, 14, 30)
fake.past_date() # Дата в прошлом
fake.future_date() # Дата в будущем
# Текст
fake.text(max_nb_chars=200) # Случайный текст до 200 символов
fake.sentence() # "This is a random sentence."
# Числа
fake.random_int(min=1, max=100) # Случайное число 1-100
fake.pydecimal(left_digits=5, right_digits=2) # Decimal для цен
# URL
fake.url() # "https://example.com"
fake.slug() # "random-slug-12345"# tests/factories.py
class ProductFactory(factory.Factory):
"""Фабрика для продуктов"""
class Meta:
model = dict
title = factory.LazyFunction(lambda: fake.sentence(nb_words=3))
description = factory.LazyFunction(lambda: fake.text(max_nb_chars=200))
price = factory.LazyFunction(lambda: fake.pydecimal(left_digits=3, right_digits=2, positive=True))
stock = factory.LazyFunction(lambda: fake.random_int(min=0, max=100))
created_at = factory.LazyFunction(lambda: fake.past_date())Использование:
product = ProductFactory()
# {
# "title": "Random product title here",
# "description": "Lorem ipsum dolor sit amet...",
# "price": Decimal('123.45'),
# "stock": 42,
# "created_at": datetime(2024, 5, 10)
# }CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER NOT NULL
);# tests/factories.py
class OrderFactory(factory.Factory):
"""Фабрика для заказов"""
class Meta:
model = dict
# ✅ user_id создаётся через UserFactory!
user = factory.SubFactory(UserFactory)
# ✅ product_id создаётся через ProductFactory!
product = factory.SubFactory(ProductFactory)
quantity = factory.LazyFunction(lambda: fake.random_int(min=1, max=10))Использование в тесте:
def test_create_order_with_subfactories(db_connection):
"""Тест создания заказа с автоматическими зависимостями"""
cur = db_connection.cursor()
# Создайте заказ через фабрику
order_data = OrderFactory()
# order_data содержит:
# {
# "user": {"email": "alice@...", "name": "Alice"},
# "product": {"title": "...", "price": 100.0},
# "quantity": 2
# }
# Вставьте user
cur.execute(
"INSERT INTO users (email, name) VALUES (%(email)s, %(name)s) RETURNING id",
order_data["user"]
)
user_id = cur.fetchone()[0]
# Вставьте product
cur.execute(
"INSERT INTO products (title, price) VALUES (%(title)s, %(price)s) RETURNING id",
order_data["product"]
)
product_id = cur.fetchone()[0]
# Вставьте order
cur.execute(
"INSERT INTO orders (user_id, product_id, quantity) VALUES (%s, %s, %s) RETURNING id",
(user_id, product_id, order_data["quantity"])
)
order_id = cur.fetchone()[0]
assert order_id is not None✅ user и product созданы автоматически!
def test_user_with_specific_email(db_connection):
"""Тест пользователя с конкретным email"""
cur = db_connection.cursor()
# Переопределите email
user_data = UserFactory(email="alice@test.com")
cur.execute(
"INSERT INTO users (email, name) VALUES (%(email)s, %(name)s)",
user_data
)
# Проверка
cur.execute("SELECT email FROM users WHERE email = %s", ("alice@test.com",))
assert cur.fetchone() is not Nonedef test_order_for_specific_user(db_connection):
"""Тест заказа для конкретного пользователя"""
cur = db_connection.cursor()
# Создайте пользователя
user_data = UserFactory(email="bob@test.com")
cur.execute(
"INSERT INTO users (email) VALUES (%(email)s) RETURNING id",
user_data
)
user_id = cur.fetchone()[0]
# Создайте заказ для этого пользователя
order_data = OrderFactory(user={"id": user_id})
# ✅ order_data["user"]["id"] == user_id# tests/conftest.py
@pytest.fixture
def create_user(db_connection):
"""Фикстура для создания пользователя"""
def _create_user(**kwargs):
cur = db_connection.cursor()
user_data = UserFactory(**kwargs)
cur.execute(
"INSERT INTO users (email, name) VALUES (%(email)s, %(name)s) RETURNING id",
user_data
)
user_id = cur.fetchone()[0]
return {"id": user_id, **user_data}
return _create_userИспользование:
def test_with_fixture(create_user):
"""Тест с фикстурой"""
user = create_user(email="test@test.com")
assert user["id"] is not None
assert user["email"] == "test@test.com"Проблема: Faker сгенерировал одинаковый email дважды.
Решение: Используйте Sequence
class UserFactory(factory.Factory):
email = factory.Sequence(lambda n: f"user{n}@test.com")
# Генерирует: user1@test.com, user2@test.com, user3@test.comПроблема: Вы создали fake один раз при импорте.
Решение: Создавайте fake внутри LazyFunction
# ❌ Плохо
fake = Faker()
name = factory.LazyFunction(fake.name) # Вызовется ОДИН раз при импорте!
# ✅ Хорошо
name = factory.LazyFunction(lambda: Faker().name()) # Вызовется каждый раз!Проблема: LazyFunction создаёт новый Faker() каждый раз.
Решение: Создайте fake один раз, но используйте через lambda
fake = Faker() # Один раз
class UserFactory(factory.Factory):
email = factory.LazyFunction(lambda: fake.email()) # lambda вызывается каждый разВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: PostgreSQL advanced: сложные сценарии