Фикстуры и параметризация в pytest
Проблема: подготовка данных
Многим тестам нужно одно и то же: пользователь, подключение к базе, временная папка, клиент API. Копировать подготовку в каждый тест плохо. В unittest для этого setUp, в pytest — фикстуры.
Фикстура
Фикстура — функция с декоратором @pytest.fixture. Тест получает её результат, просто назвав параметр её именем:
import pytest
from shop.cart import Cart
@pytest.fixture
def cart():
return Cart()
@pytest.fixture
def cart_with_items(cart): # фикстура может зависеть от другой
cart.add("чай", price=100, qty=2)
cart.add("кофе", price=250)
return cart
def test_empty_cart(cart):
assert cart.total() == 0
def test_total(cart_with_items):
assert cart_with_items.total() == 450
Это внедрение зависимостей: тест объявляет, что ему нужно, pytest сам строит граф и создаёт объекты. Каждый тест получает свежий экземпляр.
yield: подготовка и очистка
Код до yield — подготовка, после — очистка. Очистка выполнится, даже если тест упал:
@pytest.fixture
def db_connection():
conn = create_connection("sqlite:///:memory:")
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
yield conn # ← здесь выполняется тест
conn.close() # очистка
Механика та же, что у контекстных менеджеров на генераторах:
def user_fixture():
print(" setup: создаю пользователя")
user = {"name": "Анна", "balance": 100}
yield user
print(" teardown: удаляю пользователя")
def run_test(test, fixture): # упрощённо — так pytest вызывает тест
gen = fixture()
value = next(gen)
try:
test(value)
print(f" {test.__name__}: PASSED")
except AssertionError as e:
print(f" {test.__name__}: FAILED {e}")
finally:
next(gen, None) # продолжить генератор после yield → teardown
def test_balance(user):
assert user["balance"] == 100
def test_name(user):
assert user["name"] == "Боря", "имя не совпало"
run_test(test_balance, user_fixture)
run_test(test_name, user_fixture)
Области видимости
@pytest.fixture(scope="session") # один раз на весь запуск
def database():
db = start_test_database()
yield db
db.stop()
@pytest.fixture(scope="module") # один раз на файл
def api_client(database): ...
@pytest.fixture # scope="function" по умолчанию: на каждый тест
def user(database): ...
| scope | Создаётся | Пример |
|---|---|---|
function |
для каждого теста | данные, объекты под тест |
class |
на класс тестов | редкость |
module |
на файл | клиент, дорогая загрузка |
session |
на весь запуск | тестовая база, Docker-контейнер |
Широкая область — общий объект
Фикстура с scope="session" одна на все тесты. Если тест её меняет, это влияет на другие тесты. Изменяемые данные держите в function-фикстурах, а общий ресурс (базу) откатывайте после каждого теста (транзакцией).
conftest.py
Фикстуры, нужные многим файлам, кладут в conftest.py — pytest подхватит их сам, без импорта:
autouse и фабрики
@pytest.fixture(autouse=True) # применяется ко всем тестам автоматически
def reset_cache():
cache.clear()
yield
@pytest.fixture
def make_user(db): # фабрика: тест сам решает, сколько и каких
created = []
def _make(name="Анна", **fields):
user = User.create(db, name=name, **fields)
created.append(user)
return user
yield _make
for user in created:
user.delete()
def test_transfer(make_user):
alice = make_user("Алиса", balance=100)
bob = make_user("Боб")
transfer(alice, bob, 30)
assert bob.balance == 30
Встроенные фикстуры
def test_save_report(tmp_path): # временная папка (pathlib.Path)
path = tmp_path / "report.csv"
save_report(path, rows=[{"a": 1}])
assert path.read_text().startswith("a")
def test_api_key(monkeypatch): # временные подмены
monkeypatch.setenv("API_KEY", "test-key")
monkeypatch.setattr("shop.clock.now", lambda: FIXED_TIME)
assert load_settings().api_key == "test-key" # после теста всё вернётся
def test_greeting(capsys): # перехват stdout/stderr
greet("Анна")
assert capsys.readouterr().out == "Привет, Анна!\n"
def test_warning(caplog): # перехват логов
with caplog.at_level("WARNING"):
process(-1)
assert "отрицательное" in caplog.text
Параметризация
Один тест — много наборов данных:
import pytest
@pytest.mark.parametrize(
("score", "expected"),
[
(100, "A"),
(90, "A"),
(89, "B"),
(60, "C"),
(59, "F"),
(0, "F"),
],
)
def test_grade(score, expected):
assert grade(score) == expected
@pytest.mark.parametrize("bad", [-1, 101, 1000], ids=["минус", "101", "тысяча"])
def test_grade_invalid(bad):
with pytest.raises(ValueError):
grade(bad)
$ pytest -v
test_grade.py::test_grade[100-A] PASSED
test_grade.py::test_grade[90-A] PASSED
test_grade.py::test_grade[89-B] PASSED
...
test_grade.py::test_grade_invalid[минус] PASSED
Каждый набор — отдельный тест: падение одного случая не скрывает остальные. Декораторы parametrize можно ставить друг на друга — получится декартово произведение. Параметризовать можно и фикстуры: @pytest.fixture(params=["sqlite", "postgres"]).
Тестовая база данных на практике
- Django:
TestCaseоборачивает каждый тест в транзакцию и откатывает её; с pytest — плагинpytest-djangoи маркер@pytest.mark.django_db. - FastAPI + SQLAlchemy: session-фикстура поднимает тестовую базу (часто в Docker через
testcontainers), function-фикстура открывает транзакцию и откатывает после теста,app.dependency_overridesподменяет зависимостьget_db.
Вопрос на собеседовании
«Что такое фикстуры в pytest?» — Функции, подготавливающие данные или ресурсы для тестов; тест получает их по имени параметра (внедрение зависимостей). Поддерживают очистку через yield, области видимости (function, module, session), общие фикстуры в conftest.py и параметризацию.
Итог
- Фикстура —
@pytest.fixture; тест получает её по имени параметра; фикстуры зависят друг от друга. yieldразделяет подготовку и очистку; очистка выполняется и при падении.scopeуправляет временем жизни; общие фикстуры — вconftest.py.- Встроенные:
tmp_path,monkeypatch,capsys,caplog. @pytest.mark.parametrize— табличные тесты, каждый набор — отдельный тест.
Практика решено 0 из 5
Перепишите набор тестов функции slugify в табличном стиле: создайте список CASES
из пар (вход, ожидаемый результат) (не меньше 5 случаев, включая пустую строку и строку из пробелов)
и функцию test_slugify(text, expected), которая проверяет один случай.
Так работает @pytest.mark.parametrize — проверка прогонит вашу функцию по всем парам.
Какие тесты проверяют код (4)
assert len(CASES) >= 5, "Нужно не меньше 5 случаев"
assert "" in [c[0] for c in CASES] and any(c[0] and not c[0].strip() for c in CASES), "Добавьте пустую строку и строку из пробелов"
for text, expected in CASES:
test_slugify(text, expected)
broken = False
try:
test_slugify("Hello World", "WRONG")
except AssertionError:
broken = True
assert broken, "test_slugify должна падать, если результат не совпал"
def slugify(text):
return "-".join(text.lower().split())
CASES = [
("Hello World", "hello-world"),
(" Много пробелов ", "много-пробелов"),
("УЖЕ", "уже"),
("", ""),
(" ", ""),
("one", "one"),
]
def test_slugify(text, expected):
assert slugify(text) == expected
Реализуйте простую «фикстуру» в стиле pytest без самого pytest: генератор temp_db(),
который создаёт словарь-базу {"users": []}, отдаёт его через yield, а после теста
очищает — вызывает db.clear() и записывает "closed" в глобальный список LOG.
Очистка должна выполниться, даже если тест упал (используйте try/finally).
Какие тесты проверяют код (2)
LOG.clear()
gen = temp_db()
db = next(gen)
db["users"].append("Анна")
assert db == {"users": ["Анна"]}
try:
next(gen)
except StopIteration:
pass
assert db == {} and LOG == ["closed"]
LOG.clear()
gen = temp_db()
next(gen)
gen.close() # так pytest завершает фикстуру и при падении теста
assert LOG == ["closed"], "Очистка должна выполниться и при закрытии генератора"
LOG = []
def temp_db():
db = {"users": []}
try:
yield db
finally:
db.clear()
LOG.append("closed")
Что выведет pytest для такого кода?
@pytest.fixture(scope="module")
def conn():
print("connect")
yield "db"
print("disconnect")
def test_a(conn): ...
def test_b(conn): ...
(запуск с -s)
Какая встроенная фикстура pytest нужна?
| tmp_path | monkeypatch | capsys | caplog | |
|---|---|---|---|---|
| Проверить, что функция напечатала «Готово» | ||||
| Временно задать переменную окружения API_KEY | ||||
| Записать тестовый CSV-файл и прочитать его | ||||
| Проверить, что при ошибке записано WARNING-сообщение | ||||
| Подменить функцию time.time на возвращающую константу |
Для чего нужен файл conftest.py?
Войдите, чтобы отмечать пройденные уроки.