TL;DR

  • Pytest — стандартный фреймворк тестирования Python — простой синтаксис, мощные возможности
  • Пиши тесты как функции: def test_something(): с assert выражениями
  • Fixtures обрабатывают setup/teardown с декоратором @pytest.fixture
  • Параметризация для запуска теста с разными данными: @pytest.mark.parametrize
  • Богатая экосистема плагинов: pytest-cov (покрытие), pytest-xdist (параллельно), pytest-mock

Подходит для: Python разработчиков, Django/Flask/FastAPI проектов, тестирования data science Пропусти, если: Не используешь Python (используй Jest для JS, JUnit для Java)

Pytest — самый популярный фреймворк тестирования Python, используемый 65% Python-разработчиков согласно опросу JetBrains Python Developers Survey 2024. С более чем 12 000 звёзд на GitHub и 1 000+ плагинов на PyPI, pytest стал стандартом тестирования Python — от простых unit-тестов до сложных интеграционных наборов. В отличие от встроенного модуля unittest, pytest не требует шаблонных классов, предоставляет детальную интроспекцию assertions, показывающую что именно упало и почему, и предлагает мощную систему fixtures, основанную на dependency injection, а не наследовании. Крупные проекты — Django, Flask, FastAPI и Requests — все используют pytest для своих тестов. Хочешь ли ты написать свой первый Python-тест или мигрировать с unittest — это руководство охватывает установку, assertions, fixtures, параметризацию, мокирование, параллельное выполнение, интеграцию с CI/CD и реальные паттерны, которые поддерживают большие pytest-наборы.

Что такое Pytest?

Pytest — полнофункциональный фреймворк тестирования Python, позволяющий писать тесты как простые функции с обычными assert-выражениями, с автоматическим обнаружением тестов, мощными fixtures и богатой экосистемой из 1 000+ плагинов.

Согласно официальной документации pytest, pytest упрощает написание маленьких, читаемых тестов, масштабируемых до поддержки сложного функционального тестирования приложений и библиотек.

Почему pytest, а не unittest:

  • Проще синтаксис — тесты это функции, не классы
  • Лучшие assertions — используй обычный assert, получай детальные diff
  • Мощные fixtures — dependency injection без наследования
  • Параметризация — запуск одного теста с разными входными данными
  • Экосистема плагинов — 1 000+ плагинов на PyPI на любой случай
  • Автообнаружение — находит тесты автоматически

“После миграции 3 000 unittest-тестов на pytest наш тестовый код сократился на 40%, и команда начала реально писать тесты для новых фич. Низкий порог входа pytest убирает трение, которое заставляло людей избегать тестирования.” — Юрий Кан, Senior QA Lead

Установка

# Установка pytest
pip install pytest

# Проверка установки
pytest --version

Структура проекта

my-project/
├── src/
│   └── calculator.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py          # Общие fixtures
│   ├── test_calculator.py
│   └── test_utils.py
├── pytest.ini               # Конфигурация
└── requirements.txt

Pytest автоматически находит тесты в:

  • Файлах с именами test_*.py или *_test.py
  • Функциях с именами test_*
  • Классах с именами Test* с методами test_*

Написание первого теста

# src/calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# tests/test_calculator.py
from src.calculator import add, divide
import pytest

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-1, -1) == -2

def test_add_zero():
    assert add(5, 0) == 5

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

Запуск тестов

# Запустить все тесты
pytest

# Подробный вывод
pytest -v

# Запустить конкретный файл
pytest tests/test_calculator.py

# Запустить конкретный тест
pytest tests/test_calculator.py::test_add_positive_numbers

# Запустить тесты по паттерну
pytest -k "add"

# Остановиться на первом падении
pytest -x

# Показать print выражения
pytest -s

# Запустить с покрытием
pytest --cov=src

Assertions

Pytest использует встроенный в Python оператор assert с детальными сообщениями об ошибках.

Базовые Assertions

def test_assertions():
    # Равенство
    assert 1 + 1 == 2
    assert "hello" == "hello"
    assert [1, 2, 3] == [1, 2, 3]
    assert {"a": 1} == {"a": 1}

    # Истинность
    assert True
    assert not False
    assert "string"  # непустая строка истинна
    assert [1, 2]    # непустой список истинен

    # Сравнения
    assert 5 > 3
    assert 5 >= 5
    assert 3 < 5

    # Принадлежность
    assert 2 in [1, 2, 3]
    assert "hello" in "hello world"
    assert "key" in {"key": "value"}

    # Идентичность
    assert None is None
    a = [1, 2]
    b = a
    assert a is b

def test_approximate_equality():
    # Сравнение чисел с плавающей точкой
    assert 0.1 + 0.2 == pytest.approx(0.3)
    assert [0.1 + 0.2, 0.2 + 0.4] == pytest.approx([0.3, 0.6])

Тестирование исключений

import pytest

def test_raises_exception():
    with pytest.raises(ValueError):
        int("not a number")

def test_raises_with_message():
    with pytest.raises(ValueError, match="invalid literal"):
        int("not a number")

def test_exception_info():
    with pytest.raises(ValueError) as exc_info:
        int("abc")
    assert "invalid literal" in str(exc_info.value)

Fixtures

Fixtures предоставляют зависимости для тестов и обрабатывают setup/teardown.

Базовый Fixture

import pytest

@pytest.fixture
def sample_user():
    return {"name": "John", "email": "john@example.com", "age": 30}

def test_user_has_name(sample_user):
    assert sample_user["name"] == "John"

def test_user_has_email(sample_user):
    assert "@" in sample_user["email"]

Setup и Teardown

@pytest.fixture
def database_connection():
    # Setup
    connection = create_connection()

    yield connection  # Тест выполняется здесь

    # Teardown
    connection.close()

def test_query(database_connection):
    result = database_connection.query("SELECT 1")
    assert result == 1

Scope Fixtures

@pytest.fixture(scope="function")  # По умолчанию: выполняется для каждого теста
def function_fixture():
    return create_resource()

@pytest.fixture(scope="class")  # Один раз на класс тестов
def class_fixture():
    return create_resource()

@pytest.fixture(scope="module")  # Один раз на тестовый файл
def module_fixture():
    return create_resource()

@pytest.fixture(scope="session")  # Один раз на сессию тестов
def session_fixture():
    return create_resource()

Fixtures, использующие другие Fixtures

@pytest.fixture
def user():
    return {"username": "testuser", "password": "secret"}

@pytest.fixture
def authenticated_client(user):
    client = APIClient()
    client.login(user["username"], user["password"])
    return client

def test_protected_endpoint(authenticated_client):
    response = authenticated_client.get("/api/protected")
    assert response.status_code == 200

conftest.py — Общие Fixtures

# tests/conftest.py
import pytest

@pytest.fixture
def app():
    """Создает приложение для тестирования."""
    from myapp import create_app
    app = create_app(testing=True)
    return app

@pytest.fixture
def client(app):
    """Создает тестовый клиент."""
    return app.test_client()

@pytest.fixture
def db(app):
    """Создает таблицы в БД."""
    from myapp import db
    with app.app_context():
        db.create_all()
        yield db
        db.drop_all()

Fixtures в conftest.py автоматически доступны всем тестам в директории и поддиректориях.

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

Запуск одного теста с разными входными данными.

Базовая параметризация

import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
    (0, 0),
    (-1, -2),
])
def test_double(input, expected):
    assert input * 2 == expected

Множественные параметры

@pytest.mark.parametrize("a,b,expected", [
    (1, 1, 2),
    (2, 3, 5),
    (10, -5, 5),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Параметризация с ID

@pytest.mark.parametrize("email,valid", [
    ("user@example.com", True),
    ("invalid", False),
    ("@missing.com", False),
    ("user@.com", False),
], ids=["valid_email", "no_at_sign", "no_local_part", "no_domain"])
def test_email_validation(email, valid):
    assert is_valid_email(email) == valid

Маркеры

Маркеры категоризируют и выбирают тесты.

Встроенные маркеры

import pytest

@pytest.mark.skip(reason="Еще не реализовано")
def test_future_feature():
    pass

@pytest.mark.skipif(sys.platform == "win32", reason="Только Unix")
def test_unix_feature():
    pass

@pytest.mark.xfail(reason="Известный баг #123")
def test_known_bug():
    assert False  # Не провалит suite

@pytest.mark.xfail(strict=True)
def test_should_fail():
    assert False  # Должен упасть, иначе тест провален

Пользовательские маркеры

# pytest.ini
# [pytest]
# markers =
#     slow: помечает тесты как медленные
#     integration: интеграционные тесты

@pytest.mark.slow
def test_slow_operation():
    import time
    time.sleep(5)
    assert True

@pytest.mark.integration
def test_database_integration():
    # Требует базу данных
    pass
# Запустить только slow тесты
pytest -m slow

# Пропустить slow тесты
pytest -m "not slow"

# Запустить integration или slow тесты
pytest -m "integration or slow"

Моки

Используй pytest-mock или unittest.mock для мокирования.

pip install pytest-mock
# Использование pytest-mock
def test_api_call(mocker):
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = {"data": "mocked"}

    from myapp import fetch_data
    result = fetch_data()

    assert result == {"data": "mocked"}
    mock_get.assert_called_once()

# Использование unittest.mock
from unittest.mock import patch, MagicMock

def test_with_mock():
    with patch("myapp.external_service") as mock_service:
        mock_service.return_value = "mocked result"
        from myapp import process_data
        result = process_data()
        assert result == "mocked result"

Лучшие практики

1. Один Assertion на тест (в идеале)

# Менее идеально: множественные assertions
def test_user_creation():
    user = create_user("John", "john@example.com")
    assert user.name == "John"
    assert user.email == "john@example.com"
    assert user.id is not None

# Лучше: сфокусированные тесты
def test_user_has_correct_name():
    user = create_user("John", "john@example.com")
    assert user.name == "John"

def test_user_has_correct_email():
    user = create_user("John", "john@example.com")
    assert user.email == "john@example.com"

2. Описательные имена тестов

# Плохо
def test_1():
    pass

# Хорошо
def test_user_cannot_login_with_invalid_password():
    pass

3. Паттерн Arrange-Act-Assert

def test_discount_calculation():
    # Arrange
    cart = ShoppingCart()
    cart.add_item(Product("Book", 100))
    cart.add_item(Product("Pen", 10))

    # Act
    total = cart.calculate_total(discount=0.1)

    # Assert
    assert total == 99  # (100 + 10) * 0.9

ИИ в Pytest-тестировании

Согласно опросу StackOverflow Developer Survey 2025, 76% разработчиков используют ИИ-инструменты в своём рабочем процессе, и генерация тестов — среди топ-5 применений. AI инструменты могут ускорить написание тестов при правильном использовании.

Что AI делает хорошо:

  • Генерация тест-кейсов из сигнатур функций
  • Создание параметризованных тестовых данных
  • Написание boilerplate для fixtures
  • Предложение edge cases для тестирования

Что всё ещё требует людей:

  • Решения о том, какое поведение важно
  • Проверка, что тесты тестируют нужное
  • Отладка сложных взаимодействий fixtures
  • Понимание требований бизнес-логики

FAQ

Для чего используется pytest?

Pytest — самый популярный фреймворк тестирования Python. Он запускает unit-тесты, интеграционные тесты, функциональные тесты и end-to-end тесты с минимальным boilerplate кодом. Pytest используется крупными проектами: Django, Flask, FastAPI, Requests и большей частью экосистемы Python.

Pytest лучше unittest?

В большинстве случаев да. Pytest имеет более простой синтаксис (обычные функции вместо классов), лучшие сообщения об ошибках в assertions (показывает actual vs expected), мощные fixtures без наследования и богатую экосистему плагинов. Unittest является частью стандартной библиотеки Python, но требует больше кода для достижения тех же результатов.

Как запустить pytest?

Запусти pytest в терминале из корня проекта. Он автоматически обнаруживает тестовые файлы (с именами test_*.py) и тестовые функции (с именами test_*). Используй pytest -v для подробного вывода с каждым тестом. Используй pytest -k "pattern" для запуска тестов по паттерну.

Что такое pytest fixtures?

Fixtures — функции, декорированные @pytest.fixture, которые предоставляют зависимости для тестов: подключения к БД, тестовые данные, API клиенты или сконфигурированные объекты. Тесты получают fixtures как аргументы функций. Fixtures обрабатывают setup перед тестами и teardown после (используя yield).

Как запускать pytest тесты параллельно?

Установи pytest-xdist командой pip install pytest-xdist, затем запусти pytest -n auto для распределения тестов по всем доступным ядрам CPU. Для suite из 2,000 тестов на 8-ядерной машине это обычно сокращает выполнение с 5 минут до менее 90 секунд. Используй pytest -n 4 для указания точного количества workers. Параллельные тесты должны быть независимыми — общее состояние между тестами приведет к нестабильным падениям.

Как генерировать покрытие кода с pytest?

Установи pytest-cov командой pip install pytest-cov, затем запусти pytest --cov=src --cov-report=html. Это создаст HTML-отчет в htmlcov/, показывающий какие строки покрыты. Установи пороги через --cov-fail-under=80 для падения CI при снижении покрытия. Для монорепозиториев используй --cov=package1 --cov=package2 для отслеживания нескольких пакетов.

Смотрите также