Pytest для начинающих: установка, первый тест, фикстуры и параметризация

Ноутбук с размытым кодом на экране и полосой зелёных и красных отметок результатов тестов Программное обеспечение

Pytest ставится одной командой pip install pytest в виртуальное окружение проекта, а тест на нём пишется обычной функцией с ключевым словом assert: файл test_*.py, функция test_*, внутри проверка. Команда python -m pytest из корня проекта автоматически находит такие файлы, запускает тесты и при падении показывает, какие именно значения не сошлись. Все примеры проверены на Python 3.14 и pytest 9.2.

Что такое pytest и чем он удобнее unittest

Pytest это фреймворк для тестирования кода на языке Python. В стандартной библиотеке есть свой модуль unittest, но pytest нужно устанавливать отдельно. Основные отличия видны на первом же тесте:

unittest pytest
Тест Метод класса, унаследованного от unittest.TestCase Обычная функция test_...
Проверка self.assertEqual(a, b), assertTrue, assertRaises Простой assert a == b
Подготовка и очистка Методы setUp() и tearDown() Фикстуры с yield
Прогон с разными данными Цикл for i in range(...) с self.subTest() @pytest.mark.parametrize

Старые тесты можно и дальше использовать: pytest сам находит и запускает классы unittest.TestCase. Новые мы советуем писать функциями: короче, и при падении pytest разбирает выражение в assert по частям.

Установка pytest в виртуальное окружение

Ставить pytest в системный Python не стоит. Если pip отказывается ставить туда пакет, не обходите запрет ключом --break-system-packages: он может сломать инструменты операционной системы. Создайте виртуальную среду в папке проекта, активируйте её и установите pytest:

python -m venv .venv
.venv\Scripts\Activate.ps1      # Windows, PowerShell
source .venv/bin/activate       # Linux и macOS
pip install pytest
pytest --version

Последняя команда печатает pytest 9.2.0. Без активации python и pip останутся системными. PowerShell может не дать запустить Activate.ps1:

.venv\Scripts\Activate.ps1 : Невозможно загрузить файл C:\projects\project\.venv\Scripts\Activate.ps1, так как выполнение сценариев отключено в этой системе.

Для текущего окна запрет снимает Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process. Постоянное решение есть в статье про ModuleNotFoundError в Python.

Первый тест и как pytest находит тесты

Пример проекта: код приложения и тесты к нему.

project/
├── app.py
└── tests/
    └── test_app.py

Файл app.py:

def add(a, b):
    return a + b


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

Файл tests/test_app.py:

from app import add


def test_add():
    assert add(2, 3) == 5


def test_add_floats():
    assert add(0.1, 0.2) == 0.3

Без аргументов pytest ищет тесты, начиная с текущей директории (или с testpaths из конфига), обходит вложенные папки и собирает:

  • файлы с именем test_*.py или *_test.py;
  • в них функции, имя которых начинается с test;
  • методы test... в классах, название которых начинается с Test, если у класса нет __init__.

Функция check_add() или файл tests_app.py останутся невидимыми: если других тестов нет, pytest напишет collected 0 items.

Запуск и что pytest показывает при падении

Разработчик за двумя мониторами разбирает результаты запуска тестов

Тесты запускаются из папки project командой python -m pytest:

============================= test session starts =============================
platform win32 -- Python 3.14.3, pytest-9.2.0, pluggy-1.7.0
rootdir: C:\projects\project
collected 2 items

tests\test_app.py .F                                                     [100%]

================================== FAILURES ===================================
_______________________________ test_add_floats _______________________________

    def test_add_floats():
>       assert add(0.1, 0.2) == 0.3
E       assert 0.30000000000000004 == 0.3
E        +  where 0.30000000000000004 = add(0.1, 0.2)

tests\test_app.py:9: AssertionError
=========================== short test summary info ===========================
FAILED tests/test_app.py::test_add_floats - assert 0.30000000000000004 == 0.3
========================= 1 failed, 1 passed in 0.09s =========================

Точка означает пройденный тест, F упавший. Строки с E показывают, что функция вернула 0.30000000000000004: дробные числа хранятся как двоичные дроби, и сумма не равна ровно 0.3. Код верен, ошибся тест. Дробные результаты лучше сравнивать с помощью pytest.approx, он допускает маленькую погрешность: добавьте в начало файла import pytest и замените проверку на assert add(0.1, 0.2) == pytest.approx(0.3).

Почему python -m pytest, а не просто pytest? При запуске через python в sys.path попадает текущая папка. Голый pytest в такой структуре остановится ещё на сборе с ModuleNotFoundError: No module named 'app': он добавляет в sys.path папку самого тестового файла (tests, в ней нет __init__.py), а корень проекта с app.py туда не попадает.

Читайте также:  VS Code для Python: установка и настройка пошагово

Проверка исключений: pytest.raises

Вызов, который должен бросить исключение, кладут в блок with pytest.raises(...). Добавьте тест в tests/test_app.py:

import pytest
from app import divide


def test_divide_by_zero():
    with pytest.raises(ValueError, match="ноль"):
        divide(1, 0)

Параметр match ищет регулярное выражение в тексте исключения (через re.search), так что хватит куска сообщения. Если исключения не будет, тест упадёт с Failed: DID NOT RAISE ValueError.

Фикстуры pytest: подготовка данных, yield и conftest.py

Фикстура готовит то, что нужно тесту до начала проверки: тестовые данные, соединение с базой, файл. Тест запрашивает её по имени аргумента. Код после yield выполняется как очистка, когда фикстура больше не нужна. Если положить фикстуру в tests/conftest.py, её увидят все тесты папки без импорта:

import sqlite3
import pytest


@pytest.fixture(scope="module")
def db():
    print("\nоткрываем базу")
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    print("\nзакрываем базу")
    conn.close()

Файл tests/test_db.py:

def test_insert(db):
    db.execute("INSERT INTO users VALUES ('Аня')")
    count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
    assert count == 1


def test_table_is_shared(db):
    count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
    assert count == 1

Команда pytest tests/test_db.py -s -q (ключ -s отключает перехват вывода) печатает:

открываем базу
..
закрываем базу

2 passed in 0.01s

База открылась один раз на модуль, и второй тест увидел запись первого. Если запустить второй тест отдельно (pytest tests/test_db.py::test_table_is_shared), он падает с assert 0 == 1: тест зависит от соседа. Область scope бывает function (по умолчанию: фикстура всегда создаётся заново для каждого теста), class, module, package и session. Мы советуем оставлять function и расширять область только для дорогих ресурсов, которые тесты не меняют.

Параметризация: один тест с разными входными данными

Ряд одинаковых лабораторных пробирок с жидкостями разного цвета

Чтобы проверить функцию на десятке значений, не нужно копировать тест: вместо этого используется декоратор @pytest.mark.parametrize. Он принимает имена аргументов и список наборов (как устроены декораторы в Python, разобрано в статье про декораторы). Этот тест тоже идёт в tests/test_app.py:

import pytest
from app import add


@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (-1, 1, 0),
    ("ab", "c", "abc"),
])
def test_add_many(a, b, expected):
    assert add(a, b) == expected

В выводе python -m pytest tests/test_app.py -v каждый набор виден отдельной строкой со своим id:

tests/test_app.py::test_add_many[2-3-5] PASSED                           [ 66%]
tests/test_app.py::test_add_many[-1-1-0] PASSED                          [ 83%]
tests/test_app.py::test_add_many[ab-c-abc] PASSED                        [100%]

Если поставить два декоратора parametrize друг над другом, pytest прогонит все сочетания значений: x из [0, 1] и y из [2, 3] дадут четыре запуска.

Читайте также:  SSH ключи: как создать, где хранятся и как добавить на сервер

Метки: skip, skipif, xfail и свои

Стикеры разных цветов на доске в виде чек-листа без надписей

Метка вешается на тест декоратором @pytest.mark.<имя>, на фикстуры метки не действуют. skip пропускает тест всегда, skipif только если условие истинно, xfail отмечает тест, который должен упасть. Файл tests/test_marks.py:

import sys
import time
import pytest


@pytest.mark.skip(reason="функция ещё не написана")
def test_export():
    pass


@pytest.mark.skipif(sys.platform == "win32", reason="только для Linux и macOS")
def test_unix_slash():
    assert "/" in "/home/user"


@pytest.mark.xfail(reason="round() округляет 2.675 до 2.67")
def test_round():
    assert round(2.675, 2) == 2.68


@pytest.mark.slow
def test_long_job():
    time.sleep(1)
    assert True

На Windows pytest tests/test_marks.py -v покажет SKIPPED, SKIPPED, XFAIL и PASSED. Если тест с xfail неожиданно прошёл, он получает статус XPASS, а с strict=True такой результат роняет весь прогон.

slow это своя метка. Если её не зарегистрировать, pytest выдаст предупреждение PytestUnknownMarkWarning: Unknown pytest.mark.slow - is this a typo?. Регистрируйте свои метки в конфиге, тогда медленные тесты можно отключить: pytest -m "not slow". С strict_markers = true опечатка в имени метки становится ошибкой сбора.

Как запустить pytest: основные параметры

Параметр Что делает
-v Подробная информация: строка на каждый тест
-q Короткий вывод
-k add Запустить тесты, в имени которых есть add (без учёта регистра); можно -k "add and not floats"
-m slow Запустить тесты с меткой slow
-x Остановить выполнение на первом упавшем тесте
--lf Перезапустить только упавшие в прошлый раз (или все, если упавших не было)
--ff Сначала упавшие, потом все остальные
-s Не перехватывать вывод, print идёт в терминал

С файлами статьи, включая test_builtin.py ниже, в проекте 15 тестов, и python -m pytest -k add выбирает 5: collected 15 items / 10 deselected / 5 selected.

Настройка: pyproject.toml и pytest.ini

Конвейер с одинаковыми деталями и пост контроля качества

Конфиг избавляет от лишних шагов при запуске. В pyproject.toml с pytest 9.0 есть таблица [tool.pytest]:

[tool.pytest]
testpaths = ["tests"]
pythonpath = ["."]
markers = ["slow: долгие тесты"]
strict_markers = true

testpaths говорит, где искать тесты, pythonpath добавляет корень проекта в sys.path, и после этого голый pytest находит app.py. markers регистрирует метку slow. Для старых версий подойдёт [tool.pytest.ini_options] (с pytest 6.0) или файл pytest.ini с секцией [pytest].

Полезные встроенные фикстуры: tmp_path, capsys, monkeypatch

Часть фикстур есть в pytest сразу, их не нужно импортировать, достаточно назвать в аргументах. Полный список печатает pytest --fixtures. Файл tests/test_builtin.py:

import os


def test_tmp_path(tmp_path):
    report = tmp_path / "report.txt"
    report.write_text("готово", encoding="utf-8")
    assert report.read_text(encoding="utf-8") == "готово"


def test_capsys(capsys):
    print("Привет")
    assert capsys.readouterr().out == "Привет\n"


def test_monkeypatch(monkeypatch):
    monkeypatch.setenv("APP_MODE", "test")
    assert os.environ["APP_MODE"] == "test"

tmp_path даёт объект pathlib.Path с временной папкой, своей для каждого теста. capsys перехватывает то, что программа пишет в stdout и stderr. monkeypatch на время теста меняет переменные окружения, атрибуты и словари, а после теста возвращает всё как было.

Читайте также:  Jupyter Notebook: установка, запуск и работа с ячейками

Плагины: покрытие и параллельный запуск

pytest-cov (pip install pytest-cov) считает покрытие кода тестами: pytest --cov=app tests/ выводит таблицу покрытия по модулям. pytest-xdist (pip install pytest-xdist) запускает тесты в нескольких процессах: с -n auto их столько, сколько доступно процессоров.

Цветные детали пазла на столе, одну из них подставляют к собранной части

Частые вопросы о pytest

Почему pytest не видит мои тесты?

Проверьте имена: файл должен называться test_*.py или *_test.py, функция начинаться с test, класс с Test и без __init__. Если в выводе ModuleNotFoundError, запустите python -m pytest или добавьте pythonpath в конфиг.

Как запустить один тест?

Укажите файл и имя функции через ::: pytest tests/test_app.py::test_add. Метод класса указывают так же: ::TestClass::test_method.

Можно ли запускать тесты pytest в VS Code?

Да. Расширение Python показывает их в панели Testing, а включается pytest кнопкой Configure Python Tests; подробности в статье про VS Code для Python.

Оцените статью
bestprogrammer.ru
Добавить комментарий