На собесах всё чаще просят не только написать функцию, но и показать, как вы её протестируете. Де-факто стандарт в Python-проектах это pytest.
Простой тест
# test_price.py
from shop.price import apply_discount
def test_discount_reduces_price():
assert apply_discount(1000, percent=10) == 900
pytest находит файлы test_*.py и функции test_*, а обычный assert при падении показывает подробную разницу значений (pytest переписывает assert при импорте тестов). Исключения проверяют так:
import pytest
def test_negative_percent():
with pytest.raises(ValueError, match='percent'):
apply_discount(1000, percent=-5)
Параметризация
@pytest.mark.parametrize('price, percent, expected', [
(1000, 0, 1000),
(1000, 100, 0),
(999, 33, 669),
])
def test_discount(price, percent, expected):
assert apply_discount(price, percent) == expected
Один тест, много случаев, и каждый случай отображается отдельно. Краевые значения в таблице видны сразу.
Фикстуры
Фикстура готовит окружение и отдаёт его тесту по имени аргумента:
@pytest.fixture
def user(db):
u = db.create_user(email='a@b.ru')
yield u # всё после yield выполнится как очистка
db.delete(u)
def test_login(user, client):
assert client.login(user.email, 'secret').ok
scope:function(по умолчанию),class,module,session. Дорогие ресурсы (контейнер БД) создают на сессию, а данные на каждый тест.conftest.pyхранит общие фикстуры для каталога.- Встроенные:
tmp_path(временный каталог),monkeypatch,capsys,caplog.
Моки и подмены
from unittest.mock import Mock, patch
def test_sends_email(monkeypatch):
sent = []
monkeypatch.setattr('shop.notify.send_email', lambda to, text: sent.append(to))
place_order(user_email='a@b.ru')
assert sent == ['a@b.ru']
Главное правило patch: подменяют имя там, где его ищут, а не там, где объявили. Если shop/orders.py сделал from shop.notify import send_email, патчить надо shop.orders.send_email.
Моки удобны для внешних границ: HTTP, платёжный шлюз, почта, время. Мокать собственную логику и проверять, какие внутренние методы вызвались, плохая идея: тесты ломаются при каждом рефакторинге. Лучше проверять результат и состояние.
Пирамида и изоляция
- Юнит-тесты: быстрые, без сети и БД, на чистую логику.
- Интеграционные: с настоящей БД (часто через testcontainers или отдельный контейнер), транзакция на тест с откатом.
- E2E: немного, на ключевые сценарии.
Тесты не должны зависеть друг от друга и от порядка запуска. Время, случайность и внешние сервисы фиксируют: подменяют часы, задают seed, мокают HTTP.
Покрытие и полезные плагины
pytest-cov (--cov=app --cov-report=term-missing) показывает непокрытые строки. Высокое покрытие не гарантирует качество, но низкое честно показывает дыры. Ещё полезны pytest-xdist (параллельный запуск), pytest-asyncio или anyio (асинхронные тесты), freezegun или time-machine (время), hypothesis (тесты на свойствах).
Частые ошибки
- Патч не того пути импорта.
- Тест проверяет реализацию (порядок вызовов), а не поведение.
- Общие изменяемые данные между тестами, зависимость от порядка.
- Тесты ходят в настоящую сеть.
assertбез проверки, что исключение действительно было (вместоpytest.raises).
Что спросят на собесе
- Как вы тестируете функцию или эндпоинт, что такое фикстура и зачем
scope. - Как замокать внешний сервис, где делать
patch. - Что такое
parametrizeи как вы выбираете тестовые случаи. - Чем юнит-тест отличается от интеграционного, как тестировать с БД.
- Что такое покрытие и достаточно ли его.