Pytest — тестирование на Python: полное руководство

19 июня 2026
Автор

Антон Ларичев

Введение

Pytest — самый популярный фреймворк для тестирования на Python. По данным опроса JetBrains, его используют более 60% Python-разработчиков. Pytest проще в написании, чем встроенный unittest, поддерживает богатый вывод ошибок и имеет огромную экосистему плагинов.

Тестирование — это не дополнительная нагрузка, а инструмент, который экономит время: тесты находят ошибки до деплоя, дают уверенность при рефакторинге и служат документацией к коду.

Установка

pip install pytest

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

Для тестирования FastAPI-приложений дополнительно:

pip install pytest httpx

Первый тест

Pytest автоматически ищет файлы с именами test_*.py или *_test.py и функции, начинающиеся с test_.

Создайте файл calculator.py:

def add(a: float, b: float) -> float:
    return a + b

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Нельзя делить на ноль")
    return a / b

И файл test_calculator.py:

from calculator import add, divide
import pytest


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


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


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


def test_divide_normal():
    assert divide(10, 2) == 5.0


def test_divide_by_zero():
    # Проверяем, что функция бросает исключение
    with pytest.raises(ValueError, match="Нельзя делить на ноль"):
        divide(10, 0)

Запуск:

pytest test_calculator.py

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

# Запуск всех тестов в директории
pytest

Если вы хотите изучить тестирование Python и другие инструменты разработки — приходите на наш курс Python с нуля. На курсе 200+ уроков, AI-тренажёры для практики 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.

Assert — проверки в pytest

Pytest перехватывает стандартный assert и выводит подробную информацию при провале:

def test_list_content():
    result = [1, 2, 3]
    expected = [1, 2, 4]
    assert result == expected
    # Вывод при ошибке:
    # AssertionError: assert [1, 2, 3] == [1, 2, 4]
    #   At index 2 diff: 3 != 4


def test_string_content():
    name = "иван"
    assert "и" in name          # проверка вхождения
    assert name.startswith("и") # проверка начала
    assert len(name) == 4       # проверка длины


def test_approximate_float():
    # Для чисел с плавающей точкой — approx
    assert 0.1 + 0.2 == pytest.approx(0.3)

Фикстуры (Fixtures)

Фикстуры — основной способ переиспользовать код инициализации между тестами:

import pytest


@pytest.fixture
def sample_user():
    """Возвращает тестового пользователя"""
    return {
        "id": 1,
        "name": "Иван",
        "email": "ivan@example.com",
        "role": "user"
    }


@pytest.fixture
def admin_user():
    return {
        "id": 2,
        "name": "Администратор",
        "email": "admin@example.com",
        "role": "admin"
    }


def test_user_name(sample_user):
    # sample_user автоматически передаётся pytest
    assert sample_user["name"] == "Иван"


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


def test_admin_has_admin_role(admin_user):
    assert admin_user["role"] == "admin"

Область видимости фикстур (scope)

@pytest.fixture(scope="session")
def database():
    """Создаётся один раз на всю сессию тестов"""
    db = create_test_database()
    yield db  # yield вместо return позволяет добавить teardown
    db.close()  # выполнится после всех тестов


@pytest.fixture(scope="module")
def api_client():
    """Один экземпляр на файл тестов"""
    return create_client()


@pytest.fixture(scope="function")  # по умолчанию
def fresh_data():
    """Создаётся заново для каждого теста"""
    return {"count": 0}

Фикстура с setup и teardown

@pytest.fixture
def temp_file(tmp_path):
    """Создаёт временный файл и удаляет его после теста"""
    file = tmp_path / "test.txt"
    file.write_text("тестовые данные")
    
    yield file  # тест получает путь к файлу
    
    # Код после yield — это teardown (выполнится всегда)
    if file.exists():
        file.unlink()

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

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

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


@pytest.mark.parametrize("value, is_valid", [
    ("ivan@example.com", True),
    ("not-an-email", False),
    ("test@", False),
    ("", False),
])
def test_email_validation(value, is_valid):
    assert validate_email(value) == is_valid

Параметризация генерирует отдельный тест для каждого набора параметров — в выводе pytest -v они отображаются отдельными строками.

Моки с unittest.mock

Мок (Mock) — объект-заменитель, который имитирует поведение реального объекта:

from unittest.mock import Mock, patch, MagicMock


# Мокирование функции
def test_send_email():
    with patch('myapp.email.send_smtp') as mock_send:
        mock_send.return_value = True
        
        result = send_welcome_email("ivan@example.com")
        
        # Проверяем, что функция была вызвана
        mock_send.assert_called_once()
        # Проверяем аргументы вызова
        mock_send.assert_called_with(
            to="ivan@example.com",
            subject="Добро пожаловать!"
        )
        assert result is True


# Мокирование HTTP-запроса
def test_fetch_user():
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"id": 1, "name": "Иван"}
    
    with patch('requests.get', return_value=mock_response):
        user = fetch_user_from_api(1)
        assert user["name"] == "Иван"

pytest-mock (удобная обёртка)

pip install pytest-mock
def test_with_mocker(mocker):
    # mocker.patch — аналог patch, но с автоматической очисткой
    mock_get = mocker.patch('requests.get')
    mock_get.return_value.json.return_value = {"result": "ok"}
    
    result = my_function()
    assert result == "ok"

Организация тестов

conftest.py

Файл conftest.py содержит фикстуры и настройки, доступные всем тестам в директории:

my_project/
├── src/
│   └── ...
└── tests/
    ├── conftest.py       # общие фикстуры
    ├── test_users.py
    └── test_products.py
# tests/conftest.py
import pytest


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


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

Маркеры (marks)

@pytest.mark.slow
def test_heavy_operation():
    # Долгий тест
    pass

@pytest.mark.skip(reason="Известный баг, будет исправлен в v2")
def test_known_bug():
    pass

@pytest.mark.skipif(sys.platform == "win32", reason="Не работает на Windows")
def test_linux_only():
    pass

Запуск с фильтрацией:

pytest -m "not slow"     # пропустить медленные
pytest -m slow           # только медленные
pytest -k "user"         # тесты, содержащие 'user' в названии

Тестирование FastAPI

# test_api.py
import pytest
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)


def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Привет, FastAPI!"}


def test_create_user():
    user_data = {
        "name": "Иван",
        "email": "ivan@example.com",
        "age": 25
    }
    response = client.post("/users", json=user_data)
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Иван"
    assert "id" in data


def test_user_not_found():
    response = client.get("/users/99999")
    assert response.status_code == 404

Частые ошибки

  • Тесты зависят друг от друга. Каждый тест должен быть независимым. Общее состояние — через фикстуры с правильным scope.

  • Мокирование в неправильном месте. Мокировать нужно там, где имя используется, а не там, где оно определено: patch('mymodule.requests.get'), если mymodule импортирует requests.

  • Тесты без assert. Тест, который не упал — ещё не значит, что он что-то проверил. Убедитесь, что в каждом тесте есть хотя бы один assert.

  • Слишком большие тесты. Один тест — одна проверка. Если тест падает, должно быть понятно что именно сломалось.

Часто задаваемые вопросы

Pytest или unittest — что использовать?

Pytest в большинстве случаев предпочтительнее: синтаксис проще, вывод информативнее, фикстуры мощнее callbacks. unittest удобен, если нужно поддерживать Python 2 или есть ограничения на зависимости.

Как запустить только упавшие тесты?

pytest --lf (last failed) — повторяет только тесты, упавшие в прошлый раз. pytest --ff (first failed) — сначала упавшие, потом остальные.

Как измерить покрытие кода тестами?

pip install pytest-cov
pytest --cov=myapp --cov-report=html

Отчёт о покрытии сохранится в папку htmlcov/.

Заключение

Pytest — стандартный инструмент тестирования в Python-экосистеме. Начните с простых assert-тестов, добавьте фикстуры для переиспользования кода, параметризацию для проверки граничных случаев и моки для изоляции внешних зависимостей.

Для углублённого изучения Python рекомендуем курс Python с нуля. В первых модулях доступно бесплатное содержание — познакомьтесь с форматом обучения до покупки полного доступа.

Стрелочка влевоPython listing — что это и как использоватьОбработка изображений с OpenCV PythonСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonСоздание собственных декораторов в PythonВложенные функции в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий