Pytest – тестирование

онлайн тренажер по питону
Онлайн-тренажер Python для начинающих

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

Начать курс

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-проектов.

 

Новости