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) Время чтения: 15 минут
Твои unittest тесты многословны. Каждому тесту нужен класс. Assertions не показывают, что упало. Fixtures требуют иерархии наследования.
Pytest исправляет всё это. Пиши простые функции. Получай детальный вывод при падениях. Делись fixtures без наследования. Запускай тесты параллельно.
Этот туториал учит pytest с нуля — установка, assertions, fixtures, параметризация и паттерны, которые делают Python тесты поддерживаемыми.
Что такое Pytest?
Pytest — фреймворк тестирования Python, который делает написание и запуск тестов простым. Это самый популярный фреймворк в экосистеме Python.
Почему pytest, а не unittest:
- Проще синтаксис — тесты это функции, не классы
- Лучшие assertions — используй обычный
assert, получай детальные diff - Мощные fixtures — dependency injection без наследования
- Параметризация — запуск одного теста с разными входными данными
- Экосистема плагинов — 1000+ плагинов на любой случай
- Автообнаружение — находит тесты автоматически
Установка
# Установка 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
AI-Assisted Pytest
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 Advanced Techniques - Мастерство fixtures, параметризации и плагинов
- Locust Python Load Testing - Performance тестирование на Python
- Пирамида автоматизации тестирования - Где unit-тесты в твоей стратегии
- Robot Framework - Keyword-driven тестирование для Python
