Тесты: unittest и pytest

⏱ 40 мин · 🧩 заданий: 5

Зачем тесты

Тест — код, который проверяет другой код. Тесты:

  • ловят ошибки до пользователей;
  • позволяют рефакторить без страха — если всё зелёное, поведение не сломано;
  • служат документацией — показывают, как пользоваться кодом;
  • обязательны в любой команде: без тестов код-ревью не пройдёт.

Структура теста: Arrange — Act — Assert

def apply_discount(price: float, percent: int) -> float:
    if not 0 <= percent <= 100:
        raise ValueError("процент от 0 до 100")
    return round(price * (100 - percent) / 100, 2)

def test_apply_discount():
    # Arrange — подготовить данные
    price, percent = 1000, 15
    # Act — выполнить действие
    result = apply_discount(price, percent)
    # Assert — проверить результат
    assert result == 850

test_apply_discount()
print("тест пройден")

unittest — встроенный фреймворк

import unittest, io

def apply_discount(price, percent):
    if not 0 <= percent <= 100:
        raise ValueError("процент от 0 до 100")
    return round(price * (100 - percent) / 100, 2)

class TestDiscount(unittest.TestCase):
    def setUp(self):                         # перед КАЖДЫМ тестом
        self.price = 1000

    def test_regular(self):
        self.assertEqual(apply_discount(self.price, 15), 850)

    def test_zero_and_full(self):
        self.assertEqual(apply_discount(self.price, 0), 1000)
        self.assertEqual(apply_discount(self.price, 100), 0)

    def test_invalid_percent(self):
        with self.assertRaises(ValueError):
            apply_discount(self.price, 150)

    def test_float(self):
        self.assertAlmostEqual(apply_discount(0.3, 10), 0.27)

    def test_broken(self):                   # специально падающий тест
        self.assertEqual(apply_discount(self.price, 10), 950)

stream = io.StringIO()
result = unittest.TextTestRunner(stream=stream, verbosity=2).run(
    unittest.defaultTestLoader.loadTestsFromTestCase(TestDiscount))
print(stream.getvalue())
Метод Проверяет
assertEqual(a, b) / assertNotEqual a == b
assertTrue(x) / assertFalse(x) истинность
assertIs(a, b) / assertIsNone(x) идентичность
assertIn(a, b) a in b
assertIsInstance(obj, cls) тип
assertRaises(Exc) исключение (как контекстный менеджер)
assertAlmostEqual(a, b) равенство float с точностью

Запуск: python -m unittest (найдёт test*.py), python -m unittest tests.test_orders -v. Django-тесты (manage.py test) построены на unittest.

pytest — стандарт индустрии

pytest — сторонний пакет (pip install pytest), но им пользуется большинство проектов. Тесты — обычные функции с обычным assert:

# tests/test_discount.py
import pytest
from shop.pricing import apply_discount


def test_regular():
    assert apply_discount(1000, 15) == 850


def test_invalid_percent():
    with pytest.raises(ValueError, match="от 0 до 100"):
        apply_discount(1000, 150)


def test_float():
    assert apply_discount(0.3, 10) == pytest.approx(0.27)

При падении pytest показывает подробности — он переписывает assert при импорте тестов:

$ pytest -q
F..
================================ FAILURES ================================
______________________________ test_regular ______________________________

    def test_regular():
>       assert apply_discount(1000, 10) == 950
E       assert 900.0 == 950
E        +  where 900.0 = apply_discount(1000, 10)

tests/test_discount.py:6: AssertionError
1 failed, 2 passed in 0.03s

Как это выглядит в этом курсе

Задания с кодом проверяются так же: ваш код + набор assert. При ошибке вы видите выражение, фактическое и ожидаемое значение — как в pytest.

Обнаружение тестов

pytest собирает файлы test_*.py или *_test.py, в них — функции test_* и классы Test* (без __init__) с методами test_*.

Запуск

pytest                          # все тесты
pytest tests/test_orders.py     # один файл
pytest tests/test_orders.py::test_total   # один тест
pytest -k "discount and not slow"         # по подстроке имени
pytest -x                       # остановиться на первом падении
pytest --lf                     # только упавшие в прошлый раз
pytest -v / -q                  # подробно / кратко
pytest -s                       # не перехватывать print
pytest --pdb                    # отладчик в месте падения
pytest -n auto                  # параллельно (плагин pytest-xdist)
pytest --cov=shop               # покрытие (плагин pytest-cov)

Маркеры

import sys
import pytest

@pytest.mark.skip(reason="ждём исправления API")
def test_external_api(): ...

@pytest.mark.skipif(sys.platform == "win32", reason="только Linux")
def test_signals(): ...

@pytest.mark.xfail(reason="известный баг #123")
def test_rounding(): ...

@pytest.mark.slow                 # свой маркер: pytest -m "not slow"
def test_big_import(): ...

Что тестировать

def grade(score: int) -> str:
    if not 0 <= score <= 100:
        raise ValueError("score 0..100")
    if score >= 90:
        return "A"
    if score >= 75:
        return "B"
    if score >= 60:
        return "C"
    return "F"

# классы эквивалентности: по одному представителю
cases = {95: "A", 80: "B", 65: "C", 30: "F"}
# граничные значения: самые частые места ошибок
cases |= {100: "A", 90: "A", 89: "B", 75: "B", 74: "C", 60: "C", 59: "F", 0: "F"}

for score, expected in cases.items():
    assert grade(score) == expected, (score, grade(score), expected)

for bad in (-1, 101):                 # некорректный ввод
    try:
        grade(bad)
        raise AssertionError(f"нет ошибки для {bad}")
    except ValueError:
        pass
print(f"проверено {len(cases) + 2} случаев")

Чек-лист:

  • счастливый путь — типичный корректный ввод;
  • граничные значения — 0, 1, пустой список, максимум, ровно на пороге;
  • некорректный ввод — исключения и сообщения об ошибках;
  • особые случаи — None, дубликаты, юникод, отрицательные числа;
  • регрессия — каждый найденный баг сначала воспроизводится тестом, потом чинится.

Тестируйте поведение, а не реализацию

Тест проверяет, что функция делает (вход → выход, побочные эффекты), а не как (какие приватные методы вызвала). Тогда рефакторинг не ломает тесты.

Вопрос на собеседовании

«Почему pytest, а не unittest?» — Меньше шаблонного кода (функции и обычный assert), подробные сообщения об ошибках, мощные фикстуры с внедрением зависимостей, параметризация, огромная экосистема плагинов (cov, xdist, asyncio, django). При этом pytest умеет запускать и unittest-тесты.

Итог

  • Тест = Arrange → Act → Assert; хороший тест быстрый, независимый, детерминированный.
  • unittest: TestCase, setUp, assertEqual, assertRaises.
  • pytest: функции test_*, обычный assert, pytest.raises, pytest.approx, маркеры, гибкий запуск.
  • Тестируйте классы эквивалентности, границы, ошибки и регрессии; проверяйте поведение, а не реализацию.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Есть функция is_leap(year) — високосный ли год. Напишите тесты для неё: функцию test_is_leap(), содержащую assert-ы для всех важных случаев. Проверка запустит ваши тесты на правильной реализации (они должны пройти) и на трёх реализациях с ошибками (каждая должна быть поймана хотя бы одним assert).

Какие тесты проверяют код (2)
test_is_leap()  # на правильной реализации тесты должны пройти
def run_with(impl):
    global is_leap
    saved = is_leap
    is_leap = impl
    try:
        test_is_leap()
        return False
    except AssertionError:
        return True
    finally:
        is_leap = saved
assert run_with(lambda y: y % 4 == 0), "Ваши тесты не ловят ошибку: забыто правило про века (1900 — не високосный)"
assert run_with(lambda y: y % 4 == 0 and y % 100 != 0), "Ваши тесты не ловят ошибку: забыто правило 400 (2000 — високосный)"
assert run_with(lambda y: y % 2 == 0), "Ваши тесты не ловят ошибку: обычные годы (например, 2022) считаются високосными"

Задание 2 Программа на Python

Напишите класс тестов TestStack(unittest.TestCase) для готового класса Stack с тремя методами: - test_push_pop — положили 1 и 2, pop() вернул 2, потом 1; - test_len — после двух push длина 2; - test_pop_empty — pop() пустого стека бросает IndexError (используйте assertRaises). Каждый тест создаёт стек в setUp.

Какие тесты проверяют код (2)
import unittest, io
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestStack)
names = sorted(t._testMethodName for t in suite)
assert names == ["test_len", "test_pop_empty", "test_push_pop"], f"Нужны три теста, найдено: {names}"
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
assert result.wasSuccessful(), f"Тесты падают: {result.failures + result.errors}"

assert "setUp" in TestStack.__dict__, "Создавайте стек в setUp"
Задание 3 Один вариант ответа

Чем assert a == b в pytest лучше self.assertEqual(a, b) в unittest?

Задание 4 Заполнить таблицу

Как pytest находит тесты по умолчанию?

будет собранне будет собран
файл test_orders.py, функция test_total()
файл orders_test.py, функция test_total()
файл orders.py, функция test_total()
класс TestOrder с методом test_pay
класс OrderTests с методом test_pay
функция check_total() в test_orders.py
Отметьте ячейку — «+», пустая — «−».
Задание 5 Несколько вариантов

Какие свойства у хорошего модульного теста?

Войдите, чтобы отмечать пройденные уроки.