Pytest: Полное руководство по тестированию на Python
Введение в Pytest
Тестирование является критически важной частью разработки программного обеспечения. Без качественных тестов сложно гарантировать надежность кода и его стабильную работу при внесении изменений. Pytest представляет собой современный, мощный и удобный фреймворк для автоматического тестирования на Python, который значительно упрощает процесс написания и выполнения тестов.
Pytest завоевал популярность в сообществе разработчиков благодаря своему лаконичному синтаксису, богатым возможностям расширения и мощной системе фикстур. Он существенно превосходит стандартный модуль unittest по удобству использования и скорости разработки тестов.
Установка и настройка Pytest
Установка через pip
pip install pytest
Проверка установки
pytest --version
Первый простой тест
Создайте файл test_example.py:
def test_addition():
assert 2 + 2 == 4
def test_string_operations():
text = "Hello, World!"
assert "Hello" in text
assert len(text) == 13
Запуск тестов
pytest
Pytest автоматически обнаружит все тестовые файлы и функции, следующие соглашениям об именовании.
Структура и организация тестов
Соглашения об именовании
Pytest использует следующие соглашения для автоматического обнаружения тестов:
- Тестовые файлы:
test_*.pyили*_test.py - Тестовые функции:
def test_...() - Тестовые классы:
class Test...(без метода__init__) - Тестовые методы в классах:
def test_...()
Организация тестовой структуры
project/
├── src/
│ ├── calculator.py
│ └── utils.py
├── tests/
│ ├── test_calculator.py
│ ├── test_utils.py
│ └── conftest.py
└── pytest.ini
Запуск тестов с различными параметрами
# Запуск всех тестов
pytest
# Запуск тестов в конкретной директории
pytest tests/
# Запуск конкретного файла
pytest test_calculator.py
# Запуск конкретного теста
pytest test_calculator.py::test_add
# Запуск тестов в классе
pytest test_calculator.py::TestCalculator
# Запуск конкретного метода класса
pytest test_calculator.py::TestCalculator::test_multiply
Основы написания тестов
Использование assert для проверок
Pytest использует стандартный оператор assert Python для проверки условий:
def test_basic_assertions():
# Проверка равенства
assert 2 + 2 == 4
# Проверка неравенства
assert 3 * 3 != 8
# Проверка вхождения
assert "Python" in "Python testing"
# Проверка типов
assert isinstance(42, int)
# Проверка истинности
assert bool([1, 2, 3])
# Проверка ложности
assert not bool([])
Тестирование исключений
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Division by zero is not allowed")
return a / b
def test_division_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
def test_exception_message():
with pytest.raises(ValueError, match="Division by zero"):
divide(10, 0)
def test_exception_type():
with pytest.raises(ZeroDivisionError):
1 / 0
Организация тестов в классы
class TestCalculator:
def test_addition(self):
assert 2 + 3 == 5
def test_subtraction(self):
assert 5 - 3 == 2
def test_multiplication(self):
assert 3 * 4 == 12
def test_division(self):
assert 10 / 2 == 5
Фикстуры (Fixtures)
Основы фикстур
Фикстуры позволяют подготавливать данные, создавать подключения и очищать ресурсы до и после выполнения тестов:
import pytest
@pytest.fixture
def sample_user():
return {
"name": "Иван Петров",
"email": "ivan@example.com",
"age": 30
}
@pytest.fixture
def sample_list():
return [1, 2, 3, 4, 5]
def test_user_data(sample_user):
assert sample_user["name"] == "Иван Петров"
assert sample_user["age"] > 18
def test_list_operations(sample_list):
assert len(sample_list) == 5
assert sum(sample_list) == 15
Области действия фикстур
@pytest.fixture(scope="function") # По умолчанию
def function_fixture():
return "function level"
@pytest.fixture(scope="module")
def module_fixture():
return "module level"
@pytest.fixture(scope="class")
def class_fixture():
return "class level"
@pytest.fixture(scope="session")
def session_fixture():
return "session level"
Фикстуры с финализацией
@pytest.fixture
def database_connection():
# Настройка
connection = create_connection()
print("Database connection established")
yield connection
# Очистка
connection.close()
print("Database connection closed")
@pytest.fixture
def temp_file():
import tempfile
import os
# Создание временного файла
fd, path = tempfile.mkstemp()
yield path
# Удаление файла
os.close(fd)
os.unlink(path)
Параметризация тестов
Базовая параметризация
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(10, 20, 30),
(-1, 1, 0),
(0, 0, 0)
])
def test_addition(a, b, expected):
assert a + b == expected
@pytest.mark.parametrize("input_value, expected", [
("hello", "HELLO"),
("world", "WORLD"),
("Python", "PYTHON")
])
def test_string_upper(input_value, expected):
assert input_value.upper() == expected
Множественная параметризация
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [4, 5, 6])
def test_multiply(x, y):
assert x * y == y * x
Параметризация с именованными параметрами
@pytest.mark.parametrize("test_input,expected", [
pytest.param("3+5", 8, id="simple_addition"),
pytest.param("2*4", 8, id="multiplication"),
pytest.param("10-2", 8, id="subtraction")
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
Маркеры и группировка тестов
Создание пользовательских маркеров
@pytest.mark.slow
def test_slow_operation():
import time
time.sleep(2)
assert True
@pytest.mark.database
def test_database_operation():
assert True
@pytest.mark.api
def test_api_call():
assert True
Конфигурация маркеров в pytest.ini
[pytest]
markers =
slow: marks tests as slow
database: marks tests as database tests
api: marks tests as API tests
integration: marks tests as integration tests
Запуск тестов по маркерам
# Запуск только медленных тестов
pytest -m slow
# Запуск тестов базы данных
pytest -m database
# Комбинирование маркеров
pytest -m "slow and database"
pytest -m "slow or api"
pytest -m "not slow"
Пропуск тестов и ожидание ошибок
Безусловный пропуск
@pytest.mark.skip(reason="Функция еще не реализована")
def test_future_feature():
assert False
@pytest.mark.skip
def test_temporary_disabled():
assert True
Условный пропуск
import sys
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
def test_walrus_operator():
if (n := 42) > 40:
assert n == 42
@pytest.mark.skipif(not hasattr(sys, "gettrace"), reason="No trace function")
def test_debug_mode():
assert True
Ожидание ошибок
@pytest.mark.xfail
def test_known_bug():
assert 1 == 2 # Известная ошибка
@pytest.mark.xfail(reason="Bug #123: Division returns wrong result")
def test_division_bug():
assert 10 / 3 == 3.333333333333333
@pytest.mark.xfail(sys.platform == "win32", reason="Bug on Windows")
def test_windows_specific():
assert True
Работа с временными файлами и каталогами
Встроенные фикстуры для работы с файлами
def test_create_file(tmp_path):
# tmp_path - это pathlib.Path объект
test_file = tmp_path / "test.txt"
test_file.write_text("Hello, World!")
assert test_file.read_text() == "Hello, World!"
assert test_file.exists()
def test_create_directory(tmp_path):
new_dir = tmp_path / "subdir"
new_dir.mkdir()
test_file = new_dir / "nested.txt"
test_file.write_text("Nested content")
assert test_file.exists()
assert test_file.parent == new_dir
def test_temporary_directory(tmpdir):
# tmpdir - это py.path.local объект (legacy)
test_file = tmpdir.join("legacy.txt")
test_file.write("Legacy content")
assert test_file.read() == "Legacy content"
Mock-объекты и Monkeypatch
Использование monkeypatch
def get_user_id():
return 42
def get_username():
return "admin"
def test_monkeypatch_function(monkeypatch):
monkeypatch.setattr("__main__.get_user_id", lambda: 123)
assert get_user_id() == 123
def test_monkeypatch_environment(monkeypatch):
monkeypatch.setenv("TEST_VAR", "test_value")
import os
assert os.environ["TEST_VAR"] == "test_value"
def test_monkeypatch_delete_attr(monkeypatch):
monkeypatch.delattr("sys.platform")
import sys
assert not hasattr(sys, "platform")
Мокирование с pytest-mock
# pip install pytest-mock
def external_api_call():
import requests
response = requests.get("https://api.example.com/data")
return response.json()
def test_api_call_mock(mocker):
mock_response = mocker.Mock()
mock_response.json.return_value = {"status": "success"}
mocker.patch("requests.get", return_value=mock_response)
result = external_api_call()
assert result == {"status": "success"}
Конфигурация Pytest
Файл pytest.ini
[pytest]
# Дополнительные параметры командной строки
addopts = -v --tb=short --strict-markers
# Пути для поиска тестов
testpaths = tests
# Паттерны для файлов тестов
python_files = test_*.py *_test.py
# Паттерны для функций тестов
python_functions = test_*
# Паттерны для классов тестов
python_classes = Test*
# Минимальная версия pytest
minversion = 6.0
# Маркеры
markers =
slow: marks tests as slow
integration: marks tests as integration tests
unit: marks tests as unit tests
# Предупреждения
filterwarnings =
ignore::UserWarning
ignore::DeprecationWarning
Конфигурация через pyproject.toml
[tool.pytest.ini_options]
addopts = "-v --tb=short"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
python_classes = ["Test*"]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests"
]
Плагины и расширения
Популярные плагины
# Покрытие кода
pip install pytest-cov
# Django интеграция
pip install pytest-django
# Асинхронное тестирование
pip install pytest-asyncio
# Мокирование
pip install pytest-mock
# Параллельное выполнение
pip install pytest-xdist
# HTML отчеты
pip install pytest-html
Использование pytest-cov
# Запуск с покрытием
pytest --cov=myproject
# Генерация HTML отчета
pytest --cov=myproject --cov-report=html
# Исключение файлов
pytest --cov=myproject --cov-report=term-missing
Конфигурация покрытия в .coveragerc
[run]
source = .
omit =
*/tests/*
*/venv/*
setup.py
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
Отладка и анализ тестов
Опции командной строки для отладки
# Подробный вывод
pytest -v
# Очень подробный вывод
pytest -vv
# Показать stdout
pytest -s
# Остановиться на первой ошибке
pytest --maxfail=1
# Показать только последние строки трассировки
pytest --tb=short
# Показать только одну строку на ошибку
pytest --tb=line
# Не показывать трассировку
pytest --tb=no
# Показать локальные переменные
pytest --tb=long -v
Использование pdb для отладки
def test_debug_example():
x = 10
y = 20
import pdb; pdb.set_trace() # Точка останова
result = x + y
assert result == 30
# Запуск с автоматическим pdb при ошибке
pytest --pdb
# Запуск с pdb при первой ошибке
pytest --pdb --maxfail=1
Интеграция с CI/CD
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
pip install -r requirements.txt
- name: Run tests
run: |
pytest --cov=./ --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
GitLab CI
stages:
- test
test:
stage: test
image: python:3.11
script:
- pip install pytest pytest-cov
- pip install -r requirements.txt
- pytest --cov=./ --cov-report=xml --junitxml=report.xml
artifacts:
reports:
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
Продвинутые возможности
Автоматическое использование фикстур
@pytest.fixture(autouse=True)
def setup_teardown():
print("Setup before each test")
yield
print("Teardown after each test")
def test_example1():
assert True
def test_example2():
assert True
Фабрики фикстур
@pytest.fixture
def user_factory():
def _create_user(name="Test User", email="test@example.com"):
return {"name": name, "email": email}
return _create_user
def test_user_creation(user_factory):
user1 = user_factory()
user2 = user_factory(name="John", email="john@example.com")
assert user1["name"] == "Test User"
assert user2["name"] == "John"
Параметризация фикстур
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_url(request):
if request.param == "sqlite":
return "sqlite:///test.db"
elif request.param == "postgresql":
return "postgresql://user:pass@localhost/test"
elif request.param == "mysql":
return "mysql://user:pass@localhost/test"
def test_database_connection(database_url):
assert database_url.startswith(("sqlite", "postgresql", "mysql"))
Таблица основных методов и функций Pytest
| Функция/Метод | Описание | Пример использования |
|---|---|---|
pytest.main() |
Программный запуск тестов | pytest.main(['-v', 'tests/']) |
pytest.fixture() |
Декоратор для создания фикстур | @pytest.fixture def data(): return [1,2,3] |
pytest.mark.parametrize() |
Параметризация тестов | @pytest.mark.parametrize("x", [1,2,3]) |
pytest.mark.skip() |
Пропуск теста | @pytest.mark.skip(reason="Not ready") |
pytest.mark.skipif() |
Условный пропуск | @pytest.mark.skipif(sys.platform == "win32") |
pytest.mark.xfail() |
Ожидание ошибки | @pytest.mark.xfail(reason="Known bug") |
pytest.raises() |
Проверка исключений | with pytest.raises(ValueError): func() |
pytest.warns() |
Проверка предупреждений | with pytest.warns(UserWarning): func() |
pytest.deprecated_call() |
Проверка deprecated | with pytest.deprecated_call(): old_func() |
pytest.approx() |
Приблизительное сравнение | assert 0.1 + 0.2 == pytest.approx(0.3) |
pytest.fail() |
Принудительная ошибка теста | pytest.fail("Test failed for reason") |
pytest.skip() |
Пропуск теста внутри функции | if condition: pytest.skip("Skipping") |
pytest.importorskip() |
Пропуск при отсутствии модуля | np = pytest.importorskip("numpy") |
pytest.register_assert_rewrite() |
Регистрация перезаписи assert | pytest.register_assert_rewrite("mymodule") |
pytest.exit() |
Выход из pytest | pytest.exit("Exiting tests") |
Встроенные фикстуры
| Фикстура | Область | Описание |
|---|---|---|
tmp_path |
function | Временная директория (pathlib.Path) |
tmpdir |
function | Временная директория (py.path.local) |
tmp_path_factory |
session | Фабрика временных директорий |
tmpdir_factory |
session | Фабрика временных директорий (legacy) |
monkeypatch |
function | Патчинг объектов и переменных окружения |
capsys |
function | Захват stdout/stderr |
capsysbinary |
function | Захват stdout/stderr в бинарном режиме |
capfd |
function | Захват файловых дескрипторов |
capfdbinary |
function | Захват файловых дескрипторов в бинарном режиме |
caplog |
function | Захват логов |
request |
function | Информация о запросе теста |
pytestconfig |
session | Конфигурация pytest |
record_property |
function | Запись свойств для отчетов |
record_testsuite_property |
session | Запись свойств тестового набора |
recwarn |
function | Захват предупреждений |
Сравнение с другими фреймворками
| Характеристика | Pytest | unittest | nose2 | doctest |
|---|---|---|---|---|
| Синтаксис | Простой | Verbose | Средний | Встроенный |
| Фикстуры | Мощные | Ограниченные | Есть | Нет |
| Параметризация | Встроенная | Нет | Есть | Нет |
| Плагины | Множество | Мало | Средне | Нет |
| Автообнаружение | Отличное | Хорошее | Хорошее | Ограниченное |
| Отчеты | Подробные | Базовые | Хорошие | Простые |
| Производительность | Высокая | Средняя | Высокая | Низкая |
| Кривая обучения | Пологая | Крутая | Средняя | Простая |
Часто задаваемые вопросы
Что такое Pytest и чем он отличается от unittest?
Pytest - это современный фреймворк для тестирования Python, который предоставляет более простой синтаксис, мощные фикстуры и богатую экосистему плагинов по сравнению со стандартным unittest.
Как запустить только определенные тесты?
Используйте различные опции фильтрации: pytest test_file.py::test_function, pytest -k "test_name", pytest -m "marker_name".
Что такое фикстуры и зачем они нужны?
Фикстуры - это функции, которые предоставляют тестовые данные, настройки или ресурсы для тестов. Они позволяют переиспользовать код подготовки данных и обеспечивают чистоту тестов.
Как работает автоматическое обнаружение тестов?
Pytest автоматически находит файлы, соответствующие паттернам test_*.py или *_test.py, и функции/классы, начинающиеся с test_.
Можно ли запускать тесты параллельно?
Да, используйте плагин pytest-xdist: pip install pytest-xdist, затем pytest -n auto для автоматического определения количества процессов.
Как настроить покрытие кода?
Установите pytest-cov и запустите pytest --cov=myproject --cov-report=html для генерации отчета о покрытии.
Поддерживает ли Pytest асинхронные тесты?
Да, через плагин pytest-asyncio. Установите его и используйте декоратор @pytest.mark.asyncio для async функций.
Где лучше всего хранить фикстуры?
Общие фикстуры размещайте в файле conftest.py в корне тестовой директории. Pytest автоматически загружает их для всех тестов.
Как интегрировать Pytest с Django?
Используйте плагин pytest-django: pip install pytest-django, создайте pytest.ini с настройкой DJANGO_SETTINGS_MODULE.
Можно ли использовать Pytest для тестирования API?
Да, Pytest отлично подходит для тестирования API. Используйте библиотеки типа requests или httpx в сочетании с фикстурами для создания тестовых данных.
Pytest представляет собой мощный и гибкий инструмент для тестирования, который значительно упрощает написание и поддержку тестов. Его богатая экосистема плагинов и простота использования делают его оптимальным выбором для большинства Python-проектов.
Настоящее и будущее развития ИИ: классической математики уже недостаточно
Эксперты предупредили о рисках фейковой благотворительности с помощью ИИ
В России разработали универсального ИИ-агента для роботов и индустриальных процессов