pytest: тестирование кода на Python
Python pytest для начинающих: установка, первый тест, запуск и фильтры, фикстуры и conftest.py, параметризация, моки, настройка в pyproject.toml и покрытие.
Содержание · 16 разделов
- Установка pytest
- Как писать тесты: функция и assert
- Как pytest находит тесты
- Запуск тестов: основные команды
- Проверка исключений: pytest.raises
- Сравнение дробных чисел: pytest.approx
- Параметризация: один тест — много наборов данных
- Фикстуры: подготовка данных для тестов
- Простая фикстура и conftest.py
- Каждый тест получает свежую копию
- Область действия: scope
- Встроенные фикстуры
- Моки: подмена сети и внешних сервисов
- Метки: пропуск, ожидаемое падение, свои группы
- Настройка проекта: pyproject.toml и ModuleNotFoundError
- Покрытие кода: pytest-cov
- Полезные плагины
- pytest или unittest
- Частые вопросы
- Почему print() в тесте ничего не выводит?
- Как запускать тесты в VS Code или PyCharm?
- Как тестировать асинхронные функции?
- Как ускорить долгие тесты?
- Какой код возврата у pytest и как использовать его в CI?
- Как тестировать Django-проект через pytest?
- Итог
pytest — самый популярный фреймворк для тестирования кода на Python. Тест в нём — обычная функция с именем test_... и инструкцией assert, а запускает все тесты одна команда pytest. Минимальный пример Python pytest:
# prices.py
def with_vat(price: float, rate: float = 0.2) -> float:
"""Цена с НДС, округлённая до копеек."""
return round(price * (1 + rate), 2)
# test_prices.py
from prices import with_vat
def test_with_vat():
assert with_vat(1000) == 1200.0
def test_with_custom_rate():
assert with_vat(1000, rate=0.1) == 1100.0
$ pytest
============================= test session starts =============================
platform win32 -- Python 3.12.10, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\projects\prices
collected 2 items
test_prices.py .. [100%]
============================== 2 passed in 0.17s ==============================
Каждая точка — пройденный тест. Ниже — установка, правила поиска тестов, команды запуска, проверка исключений, параметризация, фикстуры, моки, настройка проекта и покрытие кода. Примеры проверены на pytest 9.1 и Python 3.12.
Установка pytest
pytest не входит в стандартную библиотеку, его ставят из PyPI — лучше в виртуальное окружение проекта:
# Windows
py -m pip install pytest
pytest --version
# macOS / Linux
python3 -m pip install pytest
pytest --version
pytest 9.1.1
pytest 9 работает на Python 3.10 и новее. На Python 3.9 pip сам поставит последнюю совместимую версию 8.x — примеры из статьи в ней тоже работают, кроме настройки через таблицу [tool.pytest] (о ней ниже).
Как писать тесты: функция и assert
Для примеров возьмём маленький проект интернет-магазина с корзиной:
shop/
├── shop/
│ ├── __init__.py
│ └── cart.py
├── tests/
│ └── test_cart.py
└── pyproject.toml
# shop/cart.py
class Cart:
def __init__(self) -> None:
self.items: dict[str, tuple[float, int]] = {}
def add(self, name: str, price: float, qty: int = 1) -> None:
if qty < 1:
raise ValueError(f"Некорректное количество: {qty}")
_, old_qty = self.items.get(name, (price, 0))
self.items[name] = (price, old_qty + qty)
def total(self) -> float:
return sum(price * qty for price, qty in self.items.values())
def discount(total: float) -> float:
"""Скидка: 5% от 5000 ₽, 10% от 10 000 ₽."""
if total >= 10_000:
return 0.10
if total >= 5_000:
return 0.05
return 0.0
Тест готовит данные, вызывает код и проверяет результат через assert. Если выражение после assert ложно, тест падает:
# tests/test_cart.py
from shop.cart import Cart
def test_empty_cart_total():
assert Cart().total() == 0
def test_add_same_item_twice():
cart = Cart()
cart.add("чай", 300)
cart.add("чай", 300, qty=2)
assert cart.items["чай"] == (300, 3)
def test_total():
cart = Cart()
cart.add("чай", 300, qty=2)
cart.add("кофе", 900)
assert cart.total() == 1600
В последнем тесте ошибка в ожидании: 2 × 300 + 900 = 1500. pytest показывает, где упала проверка и какие значения сравнивались:
tests\test_cart.py ..F [100%]
================================== FAILURES ===================================
_________________________________ test_total __________________________________
def test_total():
cart = Cart()
cart.add("чай", 300, qty=2)
cart.add("кофе", 900)
> assert cart.total() == 1600
E assert 1500 == 1600
E + where 1500 = total()
E + where total = <shop.cart.Cart object at 0x00000164D64AB320>.total
tests\test_cart.py:19: AssertionError
=========================== short test summary info ===========================
FAILED tests/test_cart.py::test_total - assert 1500 == 1600
========================= 1 failed, 2 passed in 0.10s =========================
Это главное удобство pytest: не нужно запоминать assertEqual, assertIn и другие методы — подойдёт любое выражение Python (==, in, is None, not), а pytest сам разберёт его в отчёте. Своё сообщение можно добавить через запятую: assert user.is_active, "пользователь не активирован".
Хороший тест проверяет одно поведение и называется так, чтобы по имени в отчёте было понятно, что сломалось.
Как pytest находит тесты
Без настроек pytest ищет тесты в текущей папке и во всех вложенных:
| Что | Правило |
|---|---|
| Файлы | test_*.py или *_test.py |
| Функции | имя начинается с test |
| Классы | имя начинается с Test, у класса нет метода __init__ |
| Методы в классах | имя начинается с test |
Классы unittest | любые наследники unittest.TestCase — запускаются как есть |
Если файл называется checks.py, а функция — check_total, pytest их не увидит и напишет collected 0 items и no tests ran. Это самая частая причина «pytest не находит тесты»: переименуйте файл и функции по правилам выше.
Запуск тестов: основные команды
| Команда | Что делает |
|---|---|
pytest | все тесты проекта |
pytest tests/test_cart.py | тесты одного файла |
pytest tests/test_cart.py::test_total | один тест; для метода класса — файл::Класс::метод |
pytest -k "discount and not threshold" | тесты, в имени которых есть discount и нет threshold |
pytest -m slow | тесты с меткой slow (см. раздел про метки) |
pytest -v | подробный вывод: имя и статус каждого теста |
pytest -q | краткий вывод |
pytest -x | остановиться на первом упавшем тесте |
pytest --lf | перезапустить только упавшие в прошлый раз |
pytest -s | не перехватывать вывод: print() в тестах виден в консоли |
pytest -ra | в конце показать причины пропусков и падений |
pytest --durations=5 | пять самых медленных тестов |
С -v pytest печатает идентификатор каждого теста — его можно скопировать и запустить отдельно:
$ pytest -v
tests/test_cart.py::test_empty_cart_total PASSED [ 33%]
tests/test_cart.py::test_add_same_item_twice PASSED [ 66%]
tests/test_cart.py::test_total PASSED [100%]
============================== 3 passed in 0.02s ==============================
Команду pytest можно запускать и как модуль интерпретатора: py -m pytest в Windows, python3 -m pytest в macOS и Linux. Так тесты гарантированно выполняются тем Python, в который установлен pytest, а текущая папка попадает в путь импорта — разница важна при ошибке ModuleNotFoundError (разбор ниже).
Проверка исключений: pytest.raises
Чтобы проверить, что код выбрасывает исключение, используйте контекстный менеджер pytest.raises. Параметр match — регулярное выражение, которое ищется в тексте ошибки:
# tests/test_errors.py
import pytest
from shop.cart import Cart
def test_add_zero_qty_raises():
cart = Cart()
with pytest.raises(ValueError, match="Некорректное количество"):
cart.add("чай", 300, qty=0)
def test_error_details():
with pytest.raises(ValueError) as exc_info:
Cart().add("чай", 300, qty=-1)
assert "-1" in str(exc_info.value)
Если исключения не было, тест падает с сообщением Failed: DID NOT RAISE ValueError. Подробнее о самих исключениях — в статье про try/except в Python.
Сравнение дробных чисел: pytest.approx
Числа с плавающей точкой хранятся приближённо, поэтому 0.1 + 0.2 == 0.3 ложно, и такой тест упадёт. Для сравнения с допуском есть pytest.approx:
def test_float_sum():
assert 0.1 + 0.2 == pytest.approx(0.3)
approx работает и со списками, и со словарями чисел: assert [0.1 + 0.2, 0.2 + 0.4] == pytest.approx([0.3, 0.6]).
Параметризация: один тест — много наборов данных
Чтобы не копировать тест ради разных входных данных, используйте декоратор @pytest.mark.parametrize (как устроены сами декораторы — в статье про декораторы Python). Первый аргумент — имена параметров, второй — список наборов значений:
# tests/test_discount.py
import pytest
from shop.cart import discount
@pytest.mark.parametrize(
("total", "expected"),
[
(0, 0.0),
(4999.99, 0.0),
(5000, 0.05),
(9999, 0.05),
(10_000, 0.10),
],
)
def test_discount(total, expected):
assert discount(total) == expected
@pytest.mark.parametrize(
"total",
[
pytest.param(5000, id="threshold-5"),
pytest.param(10_000, id="threshold-10"),
],
)
def test_discount_on_threshold(total):
assert discount(total) > 0
Каждый набор — отдельный тест со своим статусом:
tests/test_discount.py::test_discount[0-0.0] PASSED [ 14%]
tests/test_discount.py::test_discount[4999.99-0.0] PASSED [ 28%]
tests/test_discount.py::test_discount[5000-0.05] PASSED [ 42%]
tests/test_discount.py::test_discount[9999-0.05] PASSED [ 57%]
tests/test_discount.py::test_discount[10000-0.1] PASSED [ 71%]
tests/test_discount.py::test_discount_on_threshold[threshold-5] PASSED [ 85%]
tests/test_discount.py::test_discount_on_threshold[threshold-10] PASSED [100%]
============================== 7 passed in 0.02s ==============================
pytest.param задаёт набору понятное имя (id) или метку, например marks=pytest.mark.slow. Имена пишите латиницей: кириллицу pytest экранирует, и в отчёте вместо порог 5% появится \u043f\u043e\u0440\u043e\u0433 5%.
Фикстуры: подготовка данных для тестов
Фикстура — функция с декоратором @pytest.fixture, которая готовит данные или ресурс для теста. Тест получает фикстуру, указав её имя в аргументах, — pytest сам вызовет её перед тестом.
Простая фикстура и conftest.py
Фикстуры, нужные нескольким файлам, кладут в tests/conftest.py. Импортировать этот файл не нужно: pytest подключает его автоматически для всех тестов в папке и подпапках.
# tests/conftest.py
import sqlite3
import pytest
from shop.cart import Cart
@pytest.fixture
def cart():
"""Корзина с двумя товарами на 1500 ₽."""
c = Cart()
c.add("чай", 300, qty=2)
c.add("кофе", 900)
return c
@pytest.fixture
def db():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, total REAL)")
yield conn # тест получает соединение
conn.close() # выполнится после теста, даже если он упал
# tests/test_fixtures.py
def test_total(cart):
assert cart.total() == 1500
def test_add_more(cart):
cart.add("сахар", 100)
assert len(cart.items) == 3
def test_save_order(db, cart):
db.execute("INSERT INTO orders (total) VALUES (?)", (cart.total(),))
(count,) = db.execute("SELECT COUNT(*) FROM orders").fetchone()
assert count == 1
Фикстура с yield делит работу на две части: код до yield — подготовка, после — очистка (закрыть соединение, удалить файл, остановить сервер). Тест может запросить несколько фикстур, а фикстура — другие фикстуры.
Каждый тест получает свежую копию
По умолчанию фикстура создаётся заново для каждого теста, поэтому test_add_more не испортит корзину для test_total. Это видно с флагом --setup-show:
$ pytest tests/test_fixtures.py --setup-show
tests\test_fixtures.py
SETUP F cart
tests/test_fixtures.py::test_total (fixtures used: cart) .
TEARDOWN F cart
SETUP F cart
tests/test_fixtures.py::test_add_more (fixtures used: cart) .
TEARDOWN F cart
SETUP F db
SETUP F cart
tests/test_fixtures.py::test_save_order (fixtures used: cart, db) .
TEARDOWN F cart
TEARDOWN F db
Область действия: scope
Если подготовка дорогая (подключение к базе, запуск браузера), фикстуру можно создавать реже через параметр scope:
scope | Когда создаётся |
|---|---|
"function" (по умолчанию) | для каждого теста |
"class" | один раз на класс с тестами |
"module" | один раз на файл |
"package" | один раз на пакет (папку с тестами) |
"session" | один раз на весь запуск pytest |
@pytest.fixture(scope="module")
def connection():
print("\nподключение")
yield "conn"
print("\nотключение")
def test_one(connection):
assert connection == "conn"
def test_two(connection):
assert connection == "conn"
$ pytest -s -q
подключение
..
отключение
2 passed in 0.01s
Общую фикстуру тесты делят между собой: если один тест изменит её состояние, это увидят следующие. Поэтому широкий scope подходит для ресурсов «только для чтения» или тех, что откатываются после каждого теста.
Встроенные фикстуры
В pytest встроено около двух десятков фикстур, полный список выводит pytest --fixtures. Чаще всего нужны три:
| Фикстура | Для чего |
|---|---|
tmp_path | временная папка (pathlib.Path), своя для каждого теста |
capsys | перехват print(): capsys.readouterr().out |
monkeypatch | временная подмена атрибутов, функций, переменных окружения и sys.path |
# shop/report.py
from pathlib import Path
from shop.cart import Cart
def print_receipt(cart: Cart) -> None:
for name, (price, qty) in cart.items.items():
print(f"{name} x{qty}: {price * qty:.2f}")
print(f"Итого: {cart.total():.2f}")
def save_report(cart: Cart, path: Path) -> None:
path.write_text(f"Итого: {cart.total():.2f}\n", encoding="utf-8")
# tests/test_builtin_fixtures.py
from shop.report import print_receipt, save_report
def test_save_report(cart, tmp_path):
report = tmp_path / "report.txt"
save_report(cart, report)
assert report.read_text(encoding="utf-8") == "Итого: 1500.00\n"
def test_print_receipt(cart, capsys):
print_receipt(cart)
out = capsys.readouterr().out
assert "Итого: 1500.00" in out
Тест с tmp_path не оставляет мусора в проекте и не зависит от того, из какой папки его запустили.
Моки: подмена сети и внешних сервисов
Тесты не должны ходить в реальные API: сеть медленная и нестабильна, а данные меняются. Внешний вызов подменяют заглушкой — моком. Пусть модуль получает курс доллара из API:
# shop/rates.py
import json
import os
from urllib.request import urlopen
API_URL = os.environ.get("RATES_API", "https://example.com/api/usd")
def get_usd_rate() -> float:
"""Курс доллара из внешнего API (сетевой запрос)."""
with urlopen(API_URL, timeout=5) as resp:
return json.load(resp)["rate"]
def price_in_usd(rub: float) -> float:
return round(rub / get_usd_rate(), 2)
Через встроенную фикстуру monkeypatch функцию заменяют на время одного теста, после теста pytest вернёт оригинал:
from shop import rates
def test_price_in_usd(monkeypatch):
monkeypatch.setattr(rates, "get_usd_rate", lambda: 80.0)
assert rates.price_in_usd(1600) == 20.0
Если нужно ещё и проверить, как вызывалась заглушка, подойдёт unittest.mock из стандартной библиотеки:
from unittest.mock import patch
from shop import rates
def test_price_in_usd_with_mock():
with patch("shop.rates.get_usd_rate", return_value=80.0) as fake:
assert rates.price_in_usd(1600) == 20.0
fake.assert_called_once()
Подменяйте имя там, где его ищут во время вызова. Если модуль shop/orders.py делает from shop.rates import get_usd_rate, подменять нужно shop.orders.get_usd_rate: замена в shop.rates на уже импортированную копию не повлияет. Плагин pytest-mock даёт тот же patch в виде фикстуры mocker — без with и декораторов. Тот же приём работает для кода на requests: подменяют функцию-обёртку над запросом или requests.get.
Метки: пропуск, ожидаемое падение, свои группы
Метки (@pytest.mark....) управляют запуском отдельных тестов:
# tests/test_marks.py
import sys
import time
import pytest
@pytest.mark.skip(reason="функция ещё не написана")
def test_export_to_excel():
...
@pytest.mark.skipif(sys.platform == "win32", reason="только для Linux и macOS")
def test_unix_paths():
assert "/" in "/tmp/report.txt"
@pytest.mark.xfail(reason="2.675 хранится в float как 2.67499...")
def test_rounding():
assert round(2.675, 2) == 2.68
@pytest.mark.slow
def test_big_order():
time.sleep(1)
assert True
$ pytest tests/test_marks.py
tests\test_marks.py ssx. [100%]
=========================== short test summary info ===========================
SKIPPED [1] tests\test_marks.py:7: функция ещё не написана
SKIPPED [1] tests\test_marks.py:12: только для Linux и macOS
XFAIL tests/test_marks.py::test_rounding - 2.675 хранится в float как 2.67499...
=================== 1 passed, 2 skipped, 1 xfailed in 1.27s ===================
skip— пропустить всегда,skipif— при условии (ОС, версия Python, нет библиотеки).xfail— тест должен упасть (известная ошибка), его падение не ломает прогон. Если тест неожиданно прошёл, он помечаетсяXPASS.- Свои метки (
slow,integration,network) группируют тесты:pytest -m "not slow"запускает всё, кроме медленных.
Свои метки регистрируют в настройках (пример ниже), иначе pytest выводит предупреждение PytestUnknownMarkWarning. С опцией strict_markers = true опечатка вроде @pytest.mark.slwo становится ошибкой и не останется незамеченной.
Настройка проекта: pyproject.toml и ModuleNotFoundError
Если запустить pytest в папке shop/ без настроек, тесты упадут ещё при сборе:
$ pytest
collected 0 items / 1 error
=================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_cart.py _____________________
...
tests\test_cart.py:1: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
Причина в том, что pytest по умолчанию добавляет в путь импорта только папку с тестом (tests/). Корень проекта туда не попадает, поэтому пакет shop не находится. Есть три способа это исправить:
- запускать
python -m pytest(py -m pytestв Windows) из корня проекта — так корень попадает в путь импорта; - указать корень в настройке
pythonpath; - установить проект в окружение командой
pip install -e .— для проектов со структуройsrc/.
Надёжнее всего прописать настройки в pyproject.toml в корне проекта, тогда pytest работает из любой папки проекта и одинаково у всех разработчиков:
# pyproject.toml
[project]
name = "shop"
version = "0.1.0"
requires-python = ">=3.10"
[tool.pytest]
testpaths = ["tests"] # где искать тесты
pythonpath = ["."] # корень проекта — в путь импорта
addopts = ["-ra"] # флаги, добавляемые к каждому запуску
strict_markers = true # незарегистрированная метка — ошибка
markers = [
"slow: медленные тесты",
]
Таблица [tool.pytest] с обычными типами TOML появилась в pytest 9.0. В более старых версиях используйте [tool.pytest.ini_options] с теми же ключами, но strict_markers замените на addopts = ["-ra", "--strict-markers"]. Вместо pyproject.toml можно завести отдельный pytest.ini (раздел [pytest]) — pytest прочитает его первым.
Если ModuleNotFoundError касается сторонней библиотеки (No module named 'requests'), pytest запущен из другого интерпретатора, чем тот, куда установлен пакет: активируйте окружение проекта или запускайте python -m pytest. Подробный разбор — в статье про ModuleNotFoundError.
Покрытие кода: pytest-cov
Покрытие показывает, какие строки кода выполнялись во время тестов. Его считает плагин pytest-cov:
pip install pytest-cov
pytest --cov=shop --cov-report=term-missing
Name Stmts Miss Cover Missing
------------------------------------------------
shop\__init__.py 0 0 100%
shop\cart.py 16 0 100%
shop\rates.py 9 2 78% 10-11
shop\report.py 8 0 100%
------------------------------------------------
TOTAL 33 2 94%
В колонке Missing — номера строк, которые ни разу не выполнились. Здесь это тело get_usd_rate: в тестах мы его подменили. Высокий процент покрытия не гарантирует качество тестов, но непокрытые строки точно ничем не проверены. Отчёт в HTML: --cov-report=html, результат — в папке htmlcov/.
Полезные плагины
| Плагин | Установка | Для чего |
|---|---|---|
| pytest-cov | pip install pytest-cov | покрытие кода |
| pytest-mock | pip install pytest-mock | фикстура mocker для подмены объектов |
| pytest-xdist | pip install pytest-xdist | параллельный запуск: pytest -n auto |
| pytest-asyncio | pip install pytest-asyncio | тесты для async def-функций |
| pytest-django | pip install pytest-django | тесты Django-проектов: база данных, клиент, настройки |
Плагины подключаются сами после установки — список активных pytest выводит в строке plugins: в начале отчёта.
pytest или unittest
unittest входит в стандартную библиотеку Python и не требует установки. Тест на нём — метод класса-наследника TestCase с проверками self.assertEqual, self.assertIn и т. д.
| Критерий | pytest | unittest |
|---|---|---|
| Установка | pip install pytest | входит в Python |
| Форма теста | функция или класс | только класс-наследник TestCase |
| Проверки | обычный assert с разбором значений | методы assertEqual, assertTrue, assertRaises |
| Подготовка данных | фикстуры с зависимостями и scope | setUp / tearDown, setUpClass |
| Параметризация | @pytest.mark.parametrize | subTest в цикле |
| Плагины | сотни (покрытие, параллельность, Django) | нет системы плагинов |
| Запуск тестов другого типа | запускает тесты unittest | тесты pytest не запускает |
Для нового проекта обычно выбирают pytest: тестов пишется меньше, отчёты понятнее. Переход не требует переписывать старое — pytest запускает существующие тесты unittest без изменений:
# tests/test_unittest_style.py
import unittest
from shop.cart import Cart
class CartTestCase(unittest.TestCase):
def setUp(self):
self.cart = Cart()
def test_empty(self):
self.assertEqual(self.cart.total(), 0)
tests/test_unittest_style.py::CartTestCase::test_empty PASSED [100%]
unittest остаётся разумным выбором, когда нельзя ставить зависимости (скрипт для чужого сервера, учебная задача только на стандартной библиотеке). Внутри классов TestCase работают метки skip, skipif, xfail и autouse-фикстуры, а обычные фикстуры и параметризация pytest — нет.
Частые вопросы
Почему print() в тесте ничего не выводит?
pytest перехватывает вывод тестов и показывает его только для упавших. Чтобы увидеть print() сразу, запустите pytest -s. Если вывод нужно проверить, используйте фикстуру capsys.
Как запускать тесты в VS Code или PyCharm?
В VS Code с расширением Python откройте панель Testing (значок колбы) и выберите pytest как фреймворк — тесты появятся в дереве, их можно запускать и отлаживать по одному. Настройка редактора — в статье VS Code для Python. В PyCharm выберите pytest в Settings → Tools → Python Integrated Tools → Default test runner, после чего рядом с каждым тестом появится кнопка запуска.
Как тестировать асинхронные функции?
Установите pytest-asyncio и пометьте тест меткой @pytest.mark.asyncio, тогда внутри можно писать await:
import asyncio
import pytest
async def fetch_total() -> int:
await asyncio.sleep(0.01)
return 1500
@pytest.mark.asyncio
async def test_fetch_total():
assert await fetch_total() == 1500
Как ускорить долгие тесты?
Сначала найдите медленные: pytest --durations=10. Медленные интеграционные тесты пометьте slow и исключайте при локальной разработке (-m "not slow"). Плагин pytest-xdist распределяет тесты по ядрам процессора (pytest -n auto), но запуск рабочих процессов занимает секунды — на наборе из пары десятков быстрых тестов параллельный прогон может оказаться медленнее обычного.
Какой код возврата у pytest и как использовать его в CI?
0 — все тесты прошли, 1 — есть упавшие, 2 — запуск прерван (например, ошибка импорта при сборе), 3 — внутренняя ошибка pytest, 4 — неверные аргументы командной строки, 5 — не найдено ни одного теста. Системы CI (GitHub Actions, GitLab CI) считают шаг проваленным при любом ненулевом коде, поэтому достаточно выполнить pytest в пайплайне.
Как тестировать Django-проект через pytest?
Для Django нужен плагин pytest-django: он настраивает тестовую базу данных, даёт фикстуры client, admin_client, settings и метку django_db. Установка, настройка и примеры — в статье тестирование Django с pytest-django.
Итог
- Установите pytest в окружение проекта:
pip install pytest, запуск —pytestилиpython -m pytest. - Тест — функция
test_...в файлеtest_*.pyс обычнымassert; исключения проверяйте черезpytest.raises, дробные числа — черезpytest.approx. - Повторяющиеся проверки с разными данными объединяйте через
@pytest.mark.parametrize, подготовку данных выносите в фикстуры иconftest.py. - Сеть и внешние сервисы подменяйте через
monkeypatchилиunittest.mock. - Пропишите
testpaths,pythonpathи метки вpyproject.toml— это избавит отModuleNotFoundErrorи опечаток в метках. - Покрытие показывает pytest-cov, параллельный запуск — pytest-xdist.
Другие популярные пакеты для данных, веба и автоматизации собраны в обзоре библиотек Python.
- #Python
- #pytest
- #Тестирование
- #unittest
- #pytest-cov