Kodokon kodokon.com

Тестирование с pytest: фикстуры, параметризация, хорошие практики

Строй надёжные тесты на pytest: внедрение фикстур, очистка через yield, табличная параметризация и ловушки широких областей видимости.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

pytest держится на двух сильных идеях: обнаружение тестов по соглашению (файлы test_*.py, функции test_*) и обычный assert, интроспекция которого при провале показывает реальные значения обеих сторон - больше не нужно запоминать assertEqual и три десятка его вариантов. Два инструмента, которые стоит узнать сразу: pytest.raises, чтобы проверить, что исключение действительно возникло, и pytest.approx, чтобы сравнивать числа с плавающей точкой, не воюя с двоичным округлением.

PYTHON
import pytest

def divide(a: float, b: float) -> float:
    return a / b

def test_nominal() -> None:
    assert divide(8, 4) == 2.0

def test_zero_division() -> None:
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

def test_floats() -> None:
    assert 0.1 + 0.2 == pytest.approx(0.3)
Без approx выражение 0.1 + 0.2 == 0.3 в двоичных числах с плавающей точкой ложно.

Фикстуры - это внедрение зависимостей в pytest: объяви параметр с именем фикстуры, и pytest создаст её, а затем передаст в тест. yield разделяет подготовку и очистку - код после него выполняется, даже если тест провалился. Параметр scope (function по умолчанию, module, session) задаёт время жизни объекта. Общие фикстуры складывай в conftest.py: они становятся доступны во всей папке, без единого импорта.

PYTHON
import sqlite3

import pytest

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()

def test_insert(db) -> None:
    db.execute("INSERT INTO users VALUES ('Ada')")
    query = "SELECT name FROM users"
    rows = db.execute(query).fetchall()
    assert rows == [("Ada",)]
conn.close() выполняется после теста, прошёл он или упал.

@pytest.mark.parametrize превращает тест в таблицу случаев: каждый кортеж становится отдельным тестом с собственным именем, который можно фильтровать по отдельности. Ставь несколько декораторов parametrize друг над другом, чтобы получить декартово произведение параметров. Давай своим случаям читаемые идентификаторы через pytest.param(..., id='...') - отчёт о провале должен быть понятен без открытия исходного файла.

PYTHON
import pytest

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello World", "hello-world"),
        ("  Python  rocks ", "python-rocks"),
        pytest.param("", "", id="empty"),
    ],
)
def test_slugify(raw: str, expected: str) -> None:
    assert slugify(raw) == expected
Три случая - три отдельных теста в отчёте.
BASH
pytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5
--lf перезапускает только упавшие тесты; --durations показывает медленные.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Как pytest узнаёт, какую фикстуру передать в тест?
    • Благодаря декоратору @inject, поставленному на тест
    • По порядку объявления фикстур в файле
    • По аннотации типа у параметра
    • По имени параметра: если оно совпадает с видимой фикстурой (в модуле или в conftest.py), pytest создаёт её и передаёт в тест
  2. Для чего в фикстуре нужен код, стоящий после yield?
    • Это очистка: она выполняется после теста, даже если тест провалился
    • Он никогда не выполняется, yield завершает фикстуру
    • Он даёт запасное значение, если подготовка не удалась
    • Он выполняется, только если тест провалился
  3. Что делает команда pytest --lf?
    • Выводит список фикстур, доступных в проекте
    • Перезапускает только те тесты, которые упали в прошлый раз
    • Останавливает сессию на первом же провале