Pytest ставится одной командой pip install pytest в виртуальное окружение проекта, а тест на нём пишется обычной функцией с ключевым словом assert: файл test_*.py, функция test_*, внутри проверка. Команда python -m pytest из корня проекта автоматически находит такие файлы, запускает тесты и при падении показывает, какие именно значения не сошлись. Все примеры проверены на Python 3.14 и pytest 9.2.
- Что такое pytest и чем он удобнее unittest
- Установка pytest в виртуальное окружение
- Первый тест и как pytest находит тесты
- Запуск и что pytest показывает при падении
- Проверка исключений: pytest.raises
- Фикстуры pytest: подготовка данных, yield и conftest.py
- Параметризация: один тест с разными входными данными
- Метки: skip, skipif, xfail и свои
- Как запустить pytest: основные параметры
- Настройка: pyproject.toml и pytest.ini
- Полезные встроенные фикстуры: tmp_path, capsys, monkeypatch
- Плагины: покрытие и параллельный запуск
- Частые вопросы о pytest
- Почему pytest не видит мои тесты?
- Как запустить один тест?
- Можно ли запускать тесты pytest в VS Code?
Что такое 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 туда не попадает.
Проверка исключений: 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] дадут четыре запуска.
Метки: 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 на время теста меняет переменные окружения, атрибуты и словари, а после теста возвращает всё как было.
Плагины: покрытие и параллельный запуск
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.








