pytest — тестирование на Python

pytest — самый популярный фреймворк для написания и запуска тестов в Python. Он помогает находить ошибки до того, как они попадут в продакшн.

Уровень: средний 18 разделов ⏱ 2–3 часа

1. Что такое тестирование и pytest

Тестирование — это процесс проверки того, что программа ведёт себя правильно. В мире Python существуют два основных подхода:

ПодходОсобенности
unittestвходит в стандартную библиотеку, классы-тесты, многословный синтаксис
pytestсторонний пакет, функциональный стиль, минимум шаблонного кода

Преимущества pytest перед unittest:

Для проектов любого масштаба pytest является де-факто стандартом. Даже если вы пишете скрипты — тесты окупятся при первом рефакторинге.

2. Установка pytest

Установка выполняется одной командой через pip:

pip install pytest

Проверь, что всё установлено корректно:

pytest --version
# pytest 8.3.3

Для полноценной работы рекомендуется сразу установить дополнительные плагины:

pip install pytest-cov pytest-mock
pytest совместим с Python 3.8 и выше. Если у вас несколько версий Python, используйте python -m pytest для запуска.

3. Первый тест

pytest автоматически находит тесты, если файлы и функции названы определённым образом:

Создадим модуль 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 PASSED

4. Структура тестов и 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"] == 1000

pytest автоматически подставит фикстуру по имени параметра. Можно вызвать фикстуру вручную через аргумент 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_file

Yield-фикстуры (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 None
Yield-фикстуры выполняются даже в случае падения теста. Это гарантирует освобождение ресурсов.

6. Параметризация

Параметризация позволяет запустить один тест с набором разных входных данных.

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[девять на три] PASSED

7. Группировка и маркировка

Марки позволяют группировать тесты и запускать только нужную подмножество.

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%
Цель — не 100% покрытие любой ценой, а отсутствие непротестированных критических путей. 80–90% — хороший ориентир для большинства проектов.

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 c
def 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)

Не создавай «перегруженных» фикстур, которые тесту дают слишком много: если тесту не нужен order, зависимость не сообщай.

Зависимая фикстура автоматически даёт и «родителей»: запросив order, ты уже имеешь user и db, не перечисляя их.

14. Ещё полезные плагины

pip install pytest-cov pytest-html pytest-randomly pytest-timeout pytest-mock

Подключай их в dev-зависимости проекта, а не в прод: в requirements-dev.txt.

Покрытие — ориентир, а не цель: 100% строк с «пустыми» тестами хуже 60% по критическим сценариям.

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)
Если hypothesis нашла проблемный кейс, она сократит его до минимального примера — это готовый «репорт-баг», сохраняй его в описание.

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())
Каждый контейнер — секунды на старт. Слишком много «живых» сервисов делает suite хрупким; оставляй testcontainers для реально нужных интеграций.

17. Метрики в CI: junitxml и flaky-тесты

Понятные числа побеждают споры. Выводи результат в формате, который принимает CI:

# pytest.ini
[pytest]
addopts = --junitxml=reports/junit.xml -rN
# в CI: если тесты строго обязательны
pytest --junitxml=reports/junit.xml
$exitCode -ne 0 → прервать pipeline
Не отключай редкие «красные» ради зелёного CI: это маскирует регрессии. Лучше явный xfail с причиной, чем молчаливый удалённый тест.

18. TDD и анти-паттерны, которых избегать

TDD (сначала тест, потом код) меняет дизайн: код рождается «от потребителя». Основные ошибки новичков:

Анти-паттернЧто лучше
тест проверяет реализацию, а не поведениепроверяй контракт: возврат, ошибки, границы
гигантские фикстуры на всё сразуминимальный вход, только нужное тесту
один тест проверяет всё подрядодно поведение — один тест с понятным именем
тесты-дубликаты реализациитест на будущие изменения, а не на копию кода
Цикл TDD: красный → зелёный → рефакторинг. Красный означает «ожидаемое поведение ещё не реализовано», а не «провал CI».

19. Пользуемся конфигурацией pytest.ini

Частая рутина выносится в конфиг, чтобы запускать pytest одной командой без флагов:

# pytest.ini
[pytest]
testpaths = tests
addopts = -ra -q --strict-markers
markers =
    slow: долгие тесты, запускаются отдельно
filterwarnings =
    error
Храни конфиг в git — она становится «кодом», ревьюится и воспроизводится на любом компьютере.
Не добавляй в addopts `-p no:cacheprovider` и не отключай warnings целиком «чтобы не мешали» — быстрый путь к спрятанным регрессиям.

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 ...")
Чем больше тест изолирован (своя база, своя среда), тем меньше он падает в CI и у других разработчиков.

21. Фиксируем производительность

pytest не только «проходит/нет», но и замеряет время. Быстрые бенчмарки ловят деградацию функций:

# плагин pytest-benchmark
def test_sorting(benchmark):
    data = random_list(10000)
    benchmark(sort_list, data)
ИнструментЗачем
pytest-covпокрытие кода ветками
pytest-benchmarkрегрессии по времени
pytest-memrayутечки памяти
pytest-timeoutтесты, зависшие в цикле
Добавь порог в CI: `--cov-fail-under=80` прерывает пайплайн, когда покрытие падает ниже нормы.
Бенчмарки чувствительны к нагрузке: сравнивай результаты в одной среде, иначе «деградация» окажется шумом CI-агента.

22. Организация: пирамида тестов

Количество тестов на уровне, а скорость их — обратная. Пирамида помогает расставлять приоритеты:

УровеньСкоростьИзоляция
Unitбыстрые, мсчисто, без сети/БД
Integrationсекундыреальные сервисы (БД, API)
End-to-endминутыполный пользовательский сценарий
«Пирамида наоборот» (много e2e, мало unit) — перфоманс CI падает, а отладка провала растягивается на часы.

Что дальше

pytest — это основа тестирования на Python. Изучив фикстуры и параметризацию, вы готовы писать тесты для веб-приложений на Flask или FastAPI, а также автоматизировать проверки с помощью CI/CD.

← Вернуться к курсам