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 для отслеживания нескольких пакетов.
Смотрите также
- Pytest Advanced Techniques - Мастерство fixtures, параметризации и плагинов
- Locust Python Load Testing - Performance тестирование на Python
- Туториал по автоматизации тестирования - Основы тестирования
- Пирамида автоматизации тестирования - Где unit-тесты в твоей стратегии
- Robot Framework - Keyword-driven тестирование для Python
- Selenium Tutorial - Автоматизация браузера для Python и других языков
- Туториал по API Testing - Основы тестирования API
- Jest Testing Tutorial - Unit тестирование JavaScript если работаешь на нескольких языках
- Cypress Tutorial - E2E тестирование для веб-приложений
- Playwright vs Cypress - Сравнение E2E фреймворков
