Django-Admin.ru

pytest: тестирование кода на Python

Python pytest для начинающих: установка, первый тест, запуск и фильтры, фикстуры и conftest.py, параметризация, моки, настройка в pyproject.toml и покрытие.

Редакция Django-Admin.ru
· 18 мин чтения

Содержание · 16 разделов
  1. Установка pytest
  2. Как писать тесты: функция и assert
  3. Как pytest находит тесты
  4. Запуск тестов: основные команды
  5. Проверка исключений: pytest.raises
  6. Сравнение дробных чисел: pytest.approx
  7. Параметризация: один тест — много наборов данных
  8. Фикстуры: подготовка данных для тестов
  9. Простая фикстура и conftest.py
  10. Каждый тест получает свежую копию
  11. Область действия: scope
  12. Встроенные фикстуры
  13. Моки: подмена сети и внешних сервисов
  14. Метки: пропуск, ожидаемое падение, свои группы
  15. Настройка проекта: pyproject.toml и ModuleNotFoundError
  16. Покрытие кода: pytest-cov
  17. Полезные плагины
  18. pytest или unittest
  19. Частые вопросы
  20. Почему print() в тесте ничего не выводит?
  21. Как запускать тесты в VS Code или PyCharm?
  22. Как тестировать асинхронные функции?
  23. Как ускорить долгие тесты?
  24. Какой код возврата у pytest и как использовать его в CI?
  25. Как тестировать Django-проект через pytest?
  26. Итог

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-covpip install pytest-covпокрытие кода
pytest-mockpip install pytest-mockфикстура mocker для подмены объектов
pytest-xdistpip install pytest-xdistпараллельный запуск: pytest -n auto
pytest-asynciopip install pytest-asyncioтесты для async def-функций
pytest-djangopip install pytest-djangoтесты Django-проектов: база данных, клиент, настройки

Плагины подключаются сами после установки — список активных pytest выводит в строке plugins: в начале отчёта.

pytest или unittest

unittest входит в стандартную библиотеку Python и не требует установки. Тест на нём — метод класса-наследника TestCase с проверками self.assertEqual, self.assertIn и т. д.

Критерийpytestunittest
Установкаpip install pytestвходит в Python
Форма тестафункция или класстолько класс-наследник TestCase
Проверкиобычный assert с разбором значенийметоды assertEqual, assertTrue, assertRaises
Подготовка данныхфикстуры с зависимостями и scopesetUp / tearDown, setUpClass
Параметризация@pytest.mark.parametrizesubTest в цикле
Плагинысотни (покрытие, параллельность, 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
↑ ↓ выборEnter открытьEsc закрыть