← все темы

Тестирование в Python

Вопросов с разбором: 24. В боте эти же карточки приходят по расписанию повторений.

1 Блок 1 из 5: Основы — Какие бывают виды тестов и что такое пирамида тестирования? 🔥
- Юнит-тесты — проверяют одну единицу (функцию, класс) изолированно. Быстрые (миллисекунды), их много.
- Интеграционные — проверяют связку компонентов: код плюс реальная БД, репозиторий плюс ORM, сервис плюс брокер.
- E2E (сквозные) — весь сценарий через внешний интерфейс: HTTP API или браузер. Медленные и хрупкие, их мало.
- Ещё бывают: контрактные (совместимость API между сервисами), нагрузочные, smoke (система вообще поднялась).

Пирамида тестирования: внизу много быстрых юнит-тестов, в середине меньше интеграционных, наверху немного E2E.

Почему так: чем выше уровень, тем тест медленнее, дороже в поддержке и тем хуже указывает на место ошибки. Но юнит-тесты не ловят ошибки на стыках, поэтому нужны все уровни.

Антипаттерн «рожок мороженого»: в основном ручные и E2E-тесты, юнит-тестов почти нет.
2 Блок 1 из 5: Основы — Чем pytest отличается от unittest? 🔥
unittest — стандартная библиотека, стиль xUnit из Java:
class TestPrice(unittest.TestCase):
def setUp(self):
self.cart = Cart()

def test_empty_cart_total_is_zero(self):
self.assertEqual(self.cart.total(), 0)


pytest — сторонняя библиотека, фактический стандарт:
@pytest.fixture
def cart():
return Cart()

def test_empty_cart_total_is_zero(cart):
assert cart.total() == 0


Преимущества pytest:
- обычный assert с подробным разбором при падении (assert rewriting);
- фикстуры с внедрением зависимостей и scope вместо setUp/tearDown;
- parametrize, маркеры, богатая экосистема плагинов (pytest-django, pytest-asyncio, pytest-cov, pytest-xdist);
- запускает и тесты на unittest — можно мигрировать постепенно.
3 Блок 1 из 5: Основы — Каким должен быть хороший юнит-тест?
Структура AAA (Arrange — Act — Assert, или Given — When — Then):
def test_cancelled_order_cannot_be_paid():
order = make_order(status='cancelled') # Arrange

with pytest.raises(OrderCancelled): # Assert
order.pay(amount=500) # Act


Свойства хорошего теста (FIRST):
- Fast — быстрый, его запускают постоянно;
- Independent — не зависит от других тестов и порядка запуска;
- Repeatable — одинаковый результат везде: без реальной сети, текущего времени, случайности;
- Self-validating — сам говорит «прошёл/упал», без ручной проверки;
- Timely — пишется вместе с кодом.

Ещё:
- название описывает поведение: test_cancelled_order_cannot_be_paid, а не test_pay_2;
- одна причина упасть — проверяется одно поведение;
- проверяет поведение, а не реализацию: рефакторинг без изменения поведения не должен ломать тесты.
4 Блок 2 из 5: pytest — Что такое фикстуры в pytest и как они работают? 🔥
Фикстура — функция, которая готовит данные или окружение для теста. Тест получает её, указав имя в аргументах: pytest сам найдёт и вызовет фикстуру (внедрение зависимостей).

@pytest.fixture
def user():
return User(name='Аня', email='anya@example.com')

@pytest.fixture
def order(user): # фикстура может зависеть от другой
return Order(customer=user)

def test_order_belongs_to_user(order, user):
assert order.customer == user


Teardown через yield — код после yield выполнится после теста, даже если тест упал:
@pytest.fixture
def db_connection():
conn = connect()
yield conn
conn.close()


Где хранить: общие фикстуры кладут в conftest.py — pytest находит их автоматически для всех тестов в этой папке и вложенных, импортировать не нужно.
5 Блок 2 из 5: pytest — Что такое scope фикстуры и autouse?
Scope — как часто создаётся фикстура:
- function (по умолчанию) — для каждого теста заново;
- class — один раз на класс;
- module — один раз на файл;
- package — на пакет;
- session — один раз на весь запуск.

@pytest.fixture(scope='session')
def engine():
engine = create_engine(TEST_DB_URL)
yield engine
engine.dispose()


Правило: широкий scope — для дорогих и неизменяемых ресурсов (подключение, запущенный контейнер). Изменяемые данные — только function, иначе тесты начнут влиять друг на друга. Фикстура не может зависеть от фикстуры с более узким scope.

autouse=True — фикстура применяется ко всем тестам в области видимости без явного запроса. Подходит для глобальной подготовки: заморозить время, отключить сеть, очистить кэш.
6 Блок 2 из 5: pytest — Как работает parametrize? 🔥
@pytest.mark.parametrize запускает один тест с разными наборами данных — каждый набор становится отдельным тестом с отдельным отчётом.

@pytest.mark.parametrize('email, is_valid', [
('user@example.com', True),
('user@', False),
('', False),
('a@b.c', True),
])
def test_email_validation(email, is_valid):
assert validate_email(email) is is_valid


Полезное:
- ids=[...] или pytest.param(..., id='empty') — читаемые имена кейсов в отчёте;
- pytest.param(..., marks=pytest.mark.xfail) — пометить отдельный кейс;
- несколько декораторов parametrize дают декартово произведение;
- параметризовать можно и фикстуры: @pytest.fixture(params=['sqlite', 'postgres']).

Зачем: граничные случаи покрываются таблицей, а не копированием тестов.
7 Блок 2 из 5: pytest — Как проверить исключения, предупреждения и вывод в pytest?
def test_negative_amount_raises():
with pytest.raises(ValueError, match='отрицательн'):
Money(-1)

def test_error_details():
with pytest.raises(OrderNotFound) as exc_info:
service.get_order(999)
assert exc_info.value.order_id == 999

def test_deprecated_call_warns():
with pytest.warns(DeprecationWarning):
old_api()

def test_prints_greeting(capsys):
greet('Аня')
assert capsys.readouterr().out == 'Привет, Аня\n'

def test_logs_warning(caplog):
process(bad_item)
assert 'пропущен' in caplog.text


Важно: match — регулярное выражение по тексту исключения. Внутри with pytest.raises держат только строку, которая должна упасть, иначе тест пройдёт из-за исключения в другом месте.
8 Блок 2 из 5: pytest — Какие встроенные фикстуры pytest полезны на практике?
- tmp_path — временная папка (pathlib.Path), уникальная для теста. Для работы с файлами.
- monkeypatch — временно подменить атрибут, переменную окружения, элемент словаря; всё вернётся после теста.
- capsys / capfd — перехват stdout и stderr.
- caplog — перехват логов.
- request — информация о текущем тесте, параметры параметризованной фикстуры.

def test_reads_config_from_env(monkeypatch, tmp_path):
config_file = tmp_path / 'app.toml'
config_file.write_text('debug = true')
monkeypatch.setenv('APP_CONFIG', str(config_file))
monkeypatch.setattr('app.settings.DEFAULT_TIMEOUT', 1)

assert load_config().debug is True
9 Блок 2 из 5: pytest — Что такое маркеры skip, xfail и собственные маркеры?
@pytest.mark.skip(reason='ждём фикс в библиотеке')
def test_a(): ...

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

@pytest.mark.xfail(reason='известный баг #123', strict=True)
def test_c(): ...

@pytest.mark.slow
def test_full_import(): ...


- skip / skipif — не запускать (нет зависимости, не та платформа).
- xfail — ожидаемо падает. С strict=True неожиданный успех тоже ошибка — значит, баг починили и маркер пора снять.
- Свои маркеры (slow, integration) регистрируют в pytest.ini/pyproject.toml и выбирают при запуске: pytest -m "not slow".

Полезные флаги запуска: -k "order and not cancel" — по имени, -x — остановиться на первом падении, --lf — только упавшие в прошлый раз, -n auto — параллельно (pytest-xdist).
10 Блок 3 из 5: Моки и подмены — Что такое test doubles: dummy, stub, spy, mock, fake? 🔥
Test double — объект, который подменяет реальную зависимость в тесте. Виды (по Мезаросу и Фаулеру):

- Dummy — заглушка, которую передают, но не используют (для заполнения аргументов).
- Stub — возвращает заранее заданные ответы: «курс валют всегда 90».
- Spy — stub, который ещё и запоминает, как его вызывали.
- Mock — объект с ожиданиями: тест проверяет, что его вызвали нужным образом.
- Fake — рабочая упрощённая реализация: репозиторий на словаре, SQLite вместо Postgres, in-memory брокер.

Два стиля проверки:
- проверка состояния — после действия смотрим результат (часто с fake);
- проверка поведения — проверяем, какие вызовы были сделаны (mock).

Практика: fake часто лучше моков — тесты меньше привязаны к деталям реализации. unittest.mock.Mock в Python может играть роль stub, spy и mock одновременно.
11 Блок 3 из 5: Моки и подмены — Как работают Mock и MagicMock из unittest.mock?
Mock — объект, у которого любой атрибут и вызов возвращает новый Mock и запоминается.

from unittest.mock import Mock

gateway = Mock()
gateway.charge.return_value = {'status': 'ok'} # stub
gateway.refund.side_effect = TimeoutError # бросить исключение
gateway.get_rate.side_effect = [90, 91, 92] # ответы по очереди

service = PaymentService(gateway)
service.pay(order)

gateway.charge.assert_called_once_with(amount=500, currency='RUB') # spy/mock
assert gateway.charge.call_count == 1


- return_value — что вернуть;
- side_effect — исключение, функция или итерируемое для последовательных ответов;
- assert_called_once_with, assert_not_called, call_args_list — проверки вызовов.

MagicMock — то же, но с настроенными магическими методами (__len__, __iter__, __enter__). AsyncMock — для корутин: вызов возвращает awaitable.
12 Блок 3 из 5: Моки и подмены — Где применять patch: в каком модуле подменять объект? 🔥
Главное правило: патчить там, где объект ищут (используют), а не там, где он определён.

# app/notifications.py
from app.email import send_email

def notify(user):
send_email(user.email, 'Привет')


# неправильно: notifications уже держит свою ссылку на send_email
@patch('app.email.send_email')

# правильно: подменяем имя в модуле, который его использует
@patch('app.notifications.send_email')
def test_notify_sends_email(send_email_mock):
notify(user)
send_email_mock.assert_called_once()


Почему: from x import y создаёт в модуле собственное имя y. patch заменяет атрибут в указанном модуле, а ссылка в другом модуле остаётся старой.

Если в коде import app.email и вызов app.email.send_email(...) — тогда патчат app.email.send_email.

Формы: декоратор, контекстный менеджер with patch(...), patch.object(SomeClass, 'method'), фикстура mocker из pytest-mock.
13 Блок 3 из 5: Моки и подмены — Что такое autospec и зачем он нужен?
Обычный Mock принимает любые вызовы. Если метод переименовали или поменяли сигнатуру, тест с моком продолжит проходить, а код в проде упадёт.

gateway = Mock()
gateway.chrage(100) # опечатка — Mock молча согласится

gateway = create_autospec(PaymentGateway, instance=True)
gateway.chrage(100) # AttributeError
gateway.charge() # TypeError: не хватает аргумента amount

@patch('app.service.PaymentGateway', autospec=True)
def test_pay(gateway_cls): ...


autospec строит мок по реальному объекту: доступны только существующие атрибуты, вызовы проверяются по сигнатуре. Похожая защита — Mock(spec=PaymentGateway), но она проверяет только имена атрибутов, без сигнатур.

Рекомендация: мокать с autospec=True по умолчанию.
14 Блок 3 из 5: Моки и подмены — Когда моки вредят и что мокать, а что нет? 🔥
Мокать стоит:
- внешние системы за границей приложения: сторонние API, платёжки, почта, SMS;
- недетерминированное: время, случайность, генерацию UUID;
- медленное или опасное: отправка писем, списание денег.

Не стоит мокать:
- свой собственный код внутри тестируемого модуля — тест начинает проверять реализацию, а не поведение;
- то, чем не владеешь, напрямую (requests, boto3, ORM): лучше обернуть в свой адаптер и мокать его или подставить fake;
- value-объекты и простые структуры данных;
- БД — запросы на моках ничего не проверяют; лучше реальная тестовая БД.

Признаки злоупотребления моками:
- в тесте больше настройки моков, чем проверок;
- рефакторинг без изменения поведения ломает тесты;
- тесты зелёные, а в проде падает на стыке.

Правило: мок должен соответствовать реальному контракту (autospec, контрактные тесты), иначе тест проверяет фантазию.
15 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, работающий с базой данных? 🔥
Подходы:
- Реальная БД того же типа, что в проде (Postgres в Docker или Testcontainers). Ловит ошибки SQL, ограничений и миграций. Предпочтительно.
- SQLite вместо Postgres — быстро, но другое поведение: типы, JSON, блокировки, ON CONFLICT. Даёт ложную уверенность.
- Fake-репозиторий в памяти — для юнит-тестов бизнес-логики, а не для запросов.

Изоляция тестов:
- каждый тест в транзакции с откатом — быстро. Так делают Django TestCase и фикстура db из pytest-django;
- очистка таблиц (TRUNCATE) — когда нужно проверить реальный коммит: on_commit, несколько соединений. В Django это TransactionTestCase;
- схему создают один раз на сессию миграциями — заодно проверяются сами миграции.

@pytest.mark.django_db
def test_paid_orders_are_listed(client, order_factory):
order_factory(status='paid')
order_factory(status='new')

response = client.get('/api/orders?status=paid')

assert len(response.json()['results']) == 1
16 Блок 4 из 5: Тесты с внешним миром — Что лучше для тестовых данных: фикстуры-файлы или фабрики (factory_boy)?
Статичные фикстуры (JSON или YAML, loaddata в Django) — общий набор данных для всех тестов.
- ❌ тест зависит от данных, которых не видно в самом тесте;
- ❌ при изменении схемы файлы приходится править вручную;
- ❌ один файл обслуживает много тестов, и правка для одного ломает другие.

Фабрики (factory_boy, model_bakery) — создают объекты с разумными значениями по умолчанию; в тесте указывают только важные поля.

class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order

customer = factory.SubFactory(UserFactory)
status = 'new'
total = factory.Faker('pydecimal', left_digits=4, right_digits=2, positive=True)

def test_cancel_paid_order_creates_refund():
order = OrderFactory(status='paid') # видно, что важно для теста
...


Вывод: фабрики делают тест самодостаточным и устойчивым к изменениям схемы. Статичные данные оставляют для справочников.
17 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, который ходит во внешние HTTP API?
Варианты по уровню:
1. Свой адаптер + fake: бизнес-логика зависит от PaymentGateway, в тестах подставляется FakePaymentGateway. Самые устойчивые тесты.
2. Перехват HTTP — проверить сам адаптер: запросы, заголовки, обработку ошибок.
# httpx + respx
@respx.mock
def test_get_rate():
respx.get('https://api.rates.io/usd').respond(json={'rate': 90})
assert RatesClient().usd() == 90

# requests + responses
@responses.activate
def test_get_rate_timeout():
responses.get('https://api.rates.io/usd', body=requests.Timeout())
with pytest.raises(RatesUnavailable):
RatesClient().usd()

3. Запись и воспроизведение (vcrpy, pytest-recording) — один раз записать реальные ответы в кассету.
4. Контрактные тесты (Pact) — проверить, что мок совпадает с реальным API провайдера.

Обязательно проверить: таймауты, 5xx, 429, некорректный JSON — то, что редко случается при ручной проверке.
18 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, зависящий от времени?
Проблема: datetime.now() внутри кода делает тест недетерминированным: «подписка истекает через 30 дней» проходит или падает в зависимости от дня запуска.

Способ 1 — заморозить время:
from freezegun import freeze_time   # или time-machine — быстрее

@freeze_time('2026-01-31 23:59:00')
def test_subscription_expires_end_of_month():
sub = Subscription.start(days=1)
assert sub.expires_at == datetime(2026, 2, 1, 23, 59)


Способ 2 — передавать время или часы явно (лучше для дизайна):
def is_expired(subscription, now):
return subscription.expires_at <= now

class Service:
def __init__(self, clock=datetime.now):
self._clock = clock


Граничные случаи, которые стоит проверить: конец месяца, 29 февраля, переход через полночь, часовые пояса (UTC против локального времени).
19 Блок 4 из 5: Тесты с внешним миром — Как тестировать асинхронный код?
Обычный pytest не умеет запускать корутины. Нужен плагин: pytest-asyncio или anyio.

import pytest

@pytest.mark.asyncio
async def test_fetch_user():
client = UserClient(base_url='https://api.test')
user = await client.get(42)
assert user.id == 42

@pytest.fixture
async def redis():
conn = await create_redis()
yield conn
await conn.aclose()


- В pyproject.toml можно включить asyncio_mode = "auto", тогда маркер не нужен.
- Асинхронные зависимости мокают через AsyncMock.
- FastAPI-приложение тестируют через httpx.AsyncClient с ASGITransport или синхронным TestClient.
- Проверять стоит и таймауты, и отмену (asyncio.CancelledError), а не только успешный путь.
20 Блок 5 из 5: Качество тестов — Что такое покрытие кода и стоит ли стремиться к 100%? 🔥
Покрытие (coverage) — доля кода, выполненного во время тестов. Инструмент — coverage.py или pytest --cov.
- Line coverage — какие строки выполнились.
- Branch coverage — прошли ли обе ветки каждого if. Честнее: if x: y = 1 даёт 100% строк, даже если ветку «else» ни разу не проверили.

100% — не цель:
- покрытие показывает, что код выполнился, а не что результат проверили. Тест без assert тоже даёт покрытие;
- последние проценты дорогие и часто приводят к тестам ради цифры;
- зато низкое покрытие точно указывает на непроверенные места.

Разумный подход: высокое покрытие бизнес-логики, порог в CI, чтобы покрытие не падало (например, 80%), и проверка, что новый код покрыт. Качество тестов можно оценить мутационным тестированием (mutmut): если внесённую в код ошибку тесты не замечают — они слабые.
21 Блок 5 из 5: Качество тестов — Что такое flaky-тесты и как с ними бороться?
Flaky-тест — иногда проходит, иногда падает без изменения кода. Опасен тем, что команда перестаёт доверять красному CI.

Частые причины:
- зависимость от порядка — тест полагается на данные или состояние, оставленные другим тестом;
- время — now(), полночь, часовые пояса;
- асинхронность и гонки — sleep(1) вместо ожидания события;
- случайность без фиксированного seed;
- внешние сервисы и сеть;
- порядок без ORDER BY — БД не гарантирует порядок строк;
- общие ресурсы при параллельном запуске: порты, файлы, одна БД.

Что делать:
- воспроизвести: pytest -p randomly (случайный порядок), pytest-repeat, --count=100;
- изолировать состояние, заморозить время, ждать условие с таймаутом вместо sleep;
- не лечить ретраями — это маскирует проблему; flaky-тест чинят или временно выключают с задачей на починку.
22 Блок 5 из 5: Качество тестов — Что такое TDD?
TDD (Test-Driven Development) — разработка через тестирование. Цикл Red — Green — Refactor:
1. Red — написать маленький тест на ещё не существующее поведение и убедиться, что он падает.
2. Green — написать минимальный код, чтобы тест прошёл.
3. Refactor — улучшить код и тесты, не ломая их.

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

Минусы и ограничения:
- медленнее на старте, требует навыка;
- неудобен, когда задача исследовательская и интерфейс ещё неизвестен;
- не гарантирует хорошую архитектуру сам по себе.

На практике TDD особенно хорош для бизнес-логики с чёткими правилами и для исправления багов: сначала тест, воспроизводящий баг, потом исправление.
23 Блок 5 из 5: Качество тестов — Что такое property-based тестирование (Hypothesis)?
Обычный тест проверяет конкретные примеры. Property-based тест описывает свойство, которое верно для любых входных данных, а библиотека сама генерирует сотни случаев, включая граничные.

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_is_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)

@given(st.text())
def test_encode_decode_roundtrip(s):
assert decode(encode(s)) == s


Типичные свойства:
- обратимость: decode(encode(x)) == x;
- идемпотентность: f(f(x)) == f(x);
- инварианты: сумма не меняется, длина сохраняется;
- сравнение с простой эталонной реализацией.

Сильная сторона: находит случаи, о которых не подумали: пустые строки, юникод, огромные числа, NaN. При падении Hypothesis уменьшает пример до минимального, на котором тест падает.
24 Блок 5 из 5: Качество тестов — Как тестировать API на Django и FastAPI? 🔥
Django / DRF:
@pytest.mark.django_db
def test_create_order(api_client, user):
api_client.force_authenticate(user)

response = api_client.post('/api/orders/', {'product_id': 1, 'qty': 2}, format='json')

assert response.status_code == 201
assert Order.objects.filter(customer=user).count() == 1

django.test.Client или APIClient из DRF прогоняют запрос через middleware, URL, view и сериализатор без реального сервера.

FastAPI:
def test_get_order(client):
app.dependency_overrides[get_db] = lambda: fake_db
app.dependency_overrides[get_current_user] = lambda: User(id=1)

response = client.get('/orders/42') # client = TestClient(app)

assert response.status_code == 200
app.dependency_overrides.clear()

dependency_overrides подменяет зависимости (БД, текущего пользователя, внешние клиенты) без patch.

Что проверять: статус-коды, формат ответа и ошибок, права доступа (чужой ресурс → 403/404), валидацию, побочные эффекты в БД. Бизнес-логику при этом лучше покрывать юнит-тестами ниже по пирамиде.

Хотите не просто прочитать, а запомнить?

Бот вернёт вам эти вопросы ровно тогда, когда вы начнёте их забывать.