pytest — тестирование на Python
pytest — самый популярный фреймворк для написания и запуска тестов в Python. Он помогает находить ошибки до того, как они попадут в продакшн.
1. Что такое тестирование и pytest
Тестирование — это процесс проверки того, что программа ведёт себя правильно. В мире Python существуют два основных подхода:
| Подход | Особенности |
|---|---|
| unittest | входит в стандартную библиотеку, классы-тесты, многословный синтаксис |
| pytest | сторонний пакет, функциональный стиль, минимум шаблонного кода |
Преимущества pytest перед unittest:
- Тесты — обычные функции, не нужно наследоваться от TestCase.
- Автоматическое обнаружение тестов по именам файлов и функций.
- Мощная система фикстур для настройки окружения.
- Богатая экосистема плагинов (параметризация, моки, покрытие).
2. Установка pytest
Установка выполняется одной командой через pip:
pip install pytestПроверь, что всё установлено корректно:
pytest --version
# pytest 8.3.3Для полноценной работы рекомендуется сразу установить дополнительные плагины:
pip install pytest-cov pytest-mockpython -m pytest для запуска.3. Первый тест
pytest автоматически находит тесты, если файлы и функции названы определённым образом:
- Файлы тестов должны начинаться с
test_или заканчиваться на_test.py. - Функции тестов должны начинаться с
test_.
Создадим модуль calculator.py, который будем тестировать:
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def divide(a, b):
if b == 0:
raise ValueError("Деление на ноль запрещено")
return a / bТеперь создадим файл test_calculator.py:
from calculator import add, subtract, divide
def test_add():
assert add(2, 3) == 5
def test_subtract():
assert subtract(10, 4) == 6
def test_divide():
assert divide(10, 2) == 5.0Запуск тестов:
pytest
# collected 3 items
# test_calculator.py ... [100%]
# 3 passed in 0.03sФлаг -v включает подробный вывод с именами каждого теста:
pytest -v
# test_calculator.py::test_add PASSED
# test_calculator.py::test_subtract PASSED
# test_calculator.py::test_divide PASSED4. Структура тестов и assert
Ключевое слово assert проверяет, что выражение истинно. Если условие не выполнено, pytest показывает подробный отчёт.
def test_divide_by_zero():
try:
divide(10, 0)
assert False, "Должно было быть исключение"
except ValueError as e:
assert str(e) == "Деление на ноль запрещено"Более элегантный способ — использовать контекстный менеджер pytest.raises:
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError, match="Деление на ноль"):
divide(10, 0)Полезные варианты assert:
| Проверка | Пример |
|---|---|
| Равенство | assert result == expected |
| Неравенство | assert result != 0 |
| Булева проверка | assert is_valid |
| Содержание | assert "ошибка" in message.lower() |
| Тип | assert isinstance(result, float) |
5. Фикстуры (fixtures)
Фикстуры — это функции, которые подготавливают окружение для тестов: создают объекты, настраивают соединения, выполняют очистку.
import pytest
@pytest.fixture
def sample_account():
return {"owner": "Иван", "balance": 1000}
def test_balance(sample_account):
assert sample_account["balance"] == 1000pytest автоматически подставит фикстуру по имени параметра. Можно вызвать фикстуру вручную через аргумент request:
@pytest.fixture
def tmp_config(tmp_path):
config_file = tmp_path / "config.json"
config_file.write_text('{"debug": true}', encoding="utf-8")
return config_fileYield-фикстуры (setup / teardown)
Если фикстура содержит yield, код до yield выполняется перед тестом (setup), а после yield — после теста (teardown):
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
def test_query(db_connection):
result = db_connection.execute("SELECT 1")
assert result is not None6. Параметризация
Параметризация позволяет запустить один тест с набором разных входных данных.
import pytest
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expectedДля удобства можно задать идентификаторы тестов через параметр ids:
@pytest.mark.parametrize(
"a, b, expected",
[
(10, 2, 5),
(9, 3, 3),
],
ids=["десять на два", "девять на три"],
)
def test_divide_cases(a, b, expected):
assert divide(a, b) == expectedПри запуске с -v вместо безликих индексов будут понятные имена:
pytest -v
# test_calculator.py::test_divide_cases[десять на два] PASSED
# test_calculator.py::test_divide_cases[девять на три] PASSED7. Группировка и маркировка
Марки позволяют группировать тесты и запускать только нужную подмножество.
import pytest
@pytest.mark.slow
def test_large_computation():
result = sum(range(1_000_000))
assert result == 499999500000
def test_quick_math():
assert add(1, 1) == 2Чтобы марки работали, добавь в файл pytest.ini:
[pytest]
markers =
slow: медленные тестыЗапуск только быстрых тестов:
pytest -m "not slow"8. Моки и подмена зависимостей
Иногда тесты вызывают внешние сервисы — базу данных, API, файловую систему. Чтобы изолировать тестируемый код, используют моки.
Встроенный monkeypatch
import os
def get_env():
return os.environ.get("APP_ENV", "development")
def test_get_env(monkeypatch):
monkeypatch.setenv("APP_ENV", "testing")
assert get_env() == "testing"Плагин pytest-mock
Представим, что наш калькулятор получает курс валюты из внешнего API:
import requests
def convert_currency(amount, from_cur, to_cur):
url = f"https://api.example.com/rate?from={from_cur}&to={to_cur}"
resp = requests.get(url, timeout=5)
rate = resp.json()["rate"]
return round(amount * rate, 2)В тесте подменяем requests.get, чтобы не обращаться к реальному серверу:
def test_convert_currency(mocker):
mock_resp = mocker.Mock()
mock_resp.json.return_value = {"rate": 92.5}
mocker.patch("requests.get", return_value=mock_resp)
result = convert_currency(10, "USD", "RUB")
assert result == 925.0
mock_resp.json.assert_called_once()9. Подсчёт покрытия кода
Покрытие показывает, какой процент строк вашего кода выполняется во время тестов.
pip install pytest-covЗапуск с генерацией отчёта:
pytest --cov=calculator --cov-report=term-missingПример вывода:
# Name Stmts Miss Cover Missing
# -------------------------------------------------
# calculator.py 7 1 86% 10
# -------------------------------------------------
# 12 passed in 0.08sСтрока 10 в calculator.py не покрыта — это ветка if b == 0 в функции divide. Добавим тест:
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)Теперь покрытие вырастет до 100%:
# Name Stmts Miss Cover
# -------------------------------------------------
# calculator.py 7 0 100%10. Структура проекта с тестами
Рекомендуемая структура для реального проекта:
my_project/
calculator/
__init__.py
calculator.py
utils.py
tests/
__init__.py
conftest.py
test_calculator.py
test_utils.py
pytest.ini
requirements.txtФайл conftest.py
pytest автоматически загружает conftest.py из каждой директории. Здесь размещают общие фикстуры:
import pytest
@pytest.fixture
def sample_data():
return [
{"name": "Товар А", "price": 100},
{"name": "Товар Б", "price": 250},
{"name": "Товар В", "price": 50},
]
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()Интеграция с CI
Пример минимального конфигурационного файла .gitlab-ci.yml:
test:
image: python:3.12
script:
- pip install -r requirements.txt
- pytest --cov=calculator --cov-report=xml -v
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xmlТеперь каждый коммит автоматически запускает тесты и показывает процент покрытия в merge request.
11. Тестирование веб-приложений
Для Django и Flask окончательные фикстуры повторяют реальную инфраструктуру: клиент для запросов и временная база.
# заводим клиент один раз на сессию
@pytest.fixture(scope="session")
def client():
app.config["TESTING"] = True
with app.test_client() as c:
yield cdef test_home_ok(client):
resp = client.get("/")
assert resp.status_code == 200Для внешних сервисов подменяй сеть: responses для HTTP, mongomock для MongoDB.
--reuse-db в Django ускоряет прогон в разы, если структура таблиц не менялась.12. Параллельный запуск: pytest-xdist
Плагин pytest-xdist разгоняет тесты по процессам:
pip install pytest-xdist
pytest -n auto
pytest -n 4 # явно 4 процесса
pytest --dist loadgroup # группы держатся в одном процессеОсторожно с состоянием: общие базы данных и файлы требуют изоляции.
| Проблема | Решение |
|---|---|
| Общая БД | транзакции на фикстуру или отдельные схемы |
| Общие файлы | tmp_path на каждый тест |
| Порядок тестов | фикстуры вместо последовательности |
13. Порядок и зависимости фикстур
Фикстуры вызываются по цепочке зависимостей и кэшируются внутри scope:
@pytest.fixture
def user(db):
return User.objects.create(email="a@b.c")
@pytest.fixture
def order(user):
return Order.objects.create(user=user, total=10)- scope="function" — на каждый тест заново (по умолчанию);
- scope="module" — одна для всех тестов модуля;
- scope="session" — одна на весь запуск;
- autouse=True — выполняется без явного запроса.
Не создавай «перегруженных» фикстур, которые тесту дают слишком много: если тесту не нужен order, зависимость не сообщай.
order, ты уже имеешь user и db, не перечисляя их.14. Ещё полезные плагины
- pytest-cov — отчёт о покрытии:
pytest --cov=src; - pytest-html — красивый HTML-отчёт для CI;
- pytest-randomly — перемешивание тестов в поиске скрытых зависимостей;
- pytest-timeout — предел времени на тест, ловит зависания;
- pytest-mock — обёртка над unittest.mock в фикстуре
mocker.
pip install pytest-cov pytest-html pytest-randomly pytest-timeout pytest-mockПодключай их в dev-зависимости проекта, а не в прод: в requirements-dev.txt.
15. doсtest и property-based тесты
Пример в докстринге можно проверять автоматически — pytest умеет гонять doctest-блоки как обычные тесты:
pytest --doctest-modules src/
pytest --doctest-glob='*.rst' --doctest-modules docs/Противоположный подход — property-based тесты на hypothesis: не конкретные примеры, а сотни входов, которые ищут нарушение свойства:
from hypothesis import given, strategies as st
@given(a=st.integers(), b=st.integers())
def test_add_commutative(a, b):
assert add(a, b) == add(b, a)- doctest — дёшево и синхронно с документацией;
- hypothesis находит крайние случаи: пустые строки, None, огромные числа;
- запускай strategy-тесты реже — они медленнее обычных.
16. Внешние зависимости: factory_boy и testcontainers
Тестам нужны данные и окружение. Для генерации данных удобен factory_boy, для настоящих сервисов — testcontainers:
# factory_boy
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.Faker("email")
name = factory.Faker("name")
# testcontainers: поднять Postgres внутри теста
from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:16") as pg:
engine = create_engine(pg.get_connection_url())- factory_boy гибче словарей-фикстур и переиспользуется между тестами;
- testcontainers дают настоящую БД без ручных демонов;
- контейнер на каждый тест дорог — поднимай в fixture scope="session".
17. Метрики в CI: junitxml и flaky-тесты
Понятные числа побеждают споры. Выводи результат в формате, который принимает CI:
# pytest.ini
[pytest]
addopts = --junitxml=reports/junit.xml -rN- junit.xml потребляют плагины отчётов: Allure, pytest-html, панель CI;
- следи за временем suite: добавили тестов — а времени стало больше в разы;
- flaky-тест (то зелёный, то красный) портит доверие — отрефактори и исправь причину;
- метрика «сколько прогонов до падения» видна в логах пайплайна.
# в CI: если тесты строго обязательны
pytest --junitxml=reports/junit.xml
$exitCode -ne 0 → прервать pipeline18. TDD и анти-паттерны, которых избегать
TDD (сначала тест, потом код) меняет дизайн: код рождается «от потребителя». Основные ошибки новичков:
| Анти-паттерн | Что лучше |
|---|---|
| тест проверяет реализацию, а не поведение | проверяй контракт: возврат, ошибки, границы |
| гигантские фикстуры на всё сразу | минимальный вход, только нужное тесту |
| один тест проверяет всё подряд | одно поведение — один тест с понятным именем |
| тесты-дубликаты реализации | тест на будущие изменения, а не на копию кода |
19. Пользуемся конфигурацией pytest.ini
Частая рутина выносится в конфиг, чтобы запускать pytest одной командой без флагов:
# pytest.ini
[pytest]
testpaths = tests
addopts = -ra -q --strict-markers
markers =
slow: долгие тесты, запускаются отдельно
filterwarnings =
error- testpaths — где искать тесты, не писать путь каждый раз;
- --strict-markers запрещает необъявленные маркеры (поймает опечатку);
- filterwarnings = error превращает предупреждения в ошибки — заметно deprecated-вызовы раньше;
- для env-переменных и фабрик фикстур используй pytest-плагины вместо копипасты.
20. Тесты с базой данных
Тесты, обращающиеся к SQLite/Postgres, требуют изоляции. Транзакционные фикстуры ускоряют в разы:
@pytest.fixture
def session():
conn = psycopg.connect("...")
conn.autocommit = True
yield conn
conn.close()
def test_create_user(session):
session.execute("INSERT INTO users ...")- для Postgres поднимай реальную СУБД в Docker (testcontainers — см. раздел о них выше);
- каждый тест оставляем базу чистой: TRUNCATE в фикстуре или транзакция с откатом;
- if твой код ходит в БД через SQLAlchemy — используй его сессии в фикстуре, а не низкоуровневый драйвер;
- фактор скорости: кешируй схему (CREATE TABLE) между тестами.
21. Фиксируем производительность
pytest не только «проходит/нет», но и замеряет время. Быстрые бенчмарки ловят деградацию функций:
# плагин pytest-benchmark
def test_sorting(benchmark):
data = random_list(10000)
benchmark(sort_list, data)| Инструмент | Зачем |
|---|---|
| pytest-cov | покрытие кода ветками |
| pytest-benchmark | регрессии по времени |
| pytest-memray | утечки памяти |
| pytest-timeout | тесты, зависшие в цикле |
22. Организация: пирамида тестов
Количество тестов на уровне, а скорость их — обратная. Пирамида помогает расставлять приоритеты:
| Уровень | Скорость | Изоляция |
|---|---|---|
| Unit | быстрые, мс | чисто, без сети/БД |
| Integration | секунды | реальные сервисы (БД, API) |
| End-to-end | минуты | полный пользовательский сценарий |
- 80% тестов — unit: вот основа стабильности и скорости разработки;
- e2e держи мало и по критическим сценариям — они самые хрупкие;
- разделяй по маркерам и гони уровни в разных джобах CI.
Что дальше
pytest — это основа тестирования на Python. Изучив фикстуры и параметризацию, вы готовы писать тесты для веб-приложений на Flask или FastAPI, а также автоматизировать проверки с помощью CI/CD.