Повтор работает только на бумаге: тестируем отказ и успех через side_effect
У операции появилась повторная попытка после временного отказа. Тест со стабильно успешной заглушкой проходит, даже если цикл повторов вообще удалён. Тест со стабильно падающей функцией проверяет исчерпание попыток, но не замечает, что после первого успеха код продолжает вызывать зависимость. Нужен сценарий, где ответы меняются.
Mock принимает side_effect в виде последовательности. Исключение в очередном элементе будет поднято, обычное значение — возвращено. Так можно задать точный разговор с зависимостью: временная ошибка, затем успех. Третья попытка станет неожиданной и поможет увидеть лишнюю работу.
Отказ, затем успех
Повторяем только заранее выбранный тип временной ошибки. Для воспроизводимости передаём функцию ожидания отдельным аргументом, а в тесте заменяем её Mock. Тест не расходует секунды на sleep и при этом проверяет, что пауза запрошена между попытками. Работа с реальными часами здесь не нужна.
from unittest.mock import Mock, call
def retry(fetch, pause):
for attempt in range(2):
try:
return fetch()
except TimeoutError:
if attempt == 1:
raise
pause(0.1)
def test_recovers_once():
fetch = Mock(side_effect=[TimeoutError("slow"), "ok"])
pause = Mock()
assert retry(fetch, pause) == "ok"
assert fetch.call_count == 2
assert pause.call_args_list == [call(0.1)]
Границы политики повторов
Сохраните как test_retry.py и выполните python -m pytest -q test_retry.py после установки pytest. Тест должен пройти. Уберите break-подобный ранний return: лишний вызов либо исчерпает side_effect, либо изменит счётчик. Уберите обработку TimeoutError: до успешного второго ответа выполнение не дойдёт.
Второй необходимый сценарий — все попытки заканчиваются ошибкой. Для него задайте последовательность из двух TimeoutError, проверьте выход последней ошибки и отсутствие sleep после финальной попытки. Третий сценарий — постоянная ошибка другого типа, например ValueError: она должна выйти немедленно. Иначе повтор начнёт маскировать дефект входных данных или программирования.
Число вызовов зависимости здесь относится к контракту устойчивости: от него зависят нагрузка и возможные побочные эффекты. Но тест не доказывает, что повтор безопасен для любой операции. Для создания платежа, отправки письма и других изменений сначала нужно определить идемпотентность или ключ дедупликации. Последовательность ответов полезна и в таком тесте, если проверяется один и тот же ключ каждой попытки.
Если политика станет сложнее, отделите решение «повторять ли» и расчёт задержки от вызова зависимости. Их можно проверить обычными входными значениями. Не нужно превращать каждый тест в сценарий из десятка mock-объектов. Один короткий сценарий отказа и восстановления показывает, ради какого поведения вообще существует цикл, и делает случайный бесконечный повтор заметным.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
