ТестированиеОсновы тестированияИнженер по автоматизированному тестированию

Представьте функцию, проверяющую срок действия объекта. Тест должен надёжно проверить поведение ровно в мом...

Представьте функцию, проверяющую срок действия объекта. Тест должен надёжно проверить поведение ровно в момент истечения срока. Как изменить границу функции, чтобы результат не зависел от системных часов?

from datetime import datetime, timezone

def is_expired(expires_at):
    return datetime.now(timezone.utc) >= expires_at
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно убрать прямую зависимость функции от текущего времени и передавать источник времени извне. Такой приём называется внедрением зависимости и повышает тестируемость: тест получает контролируемое время и становится детерминированным.

Не следует проверять этот случай через sleep: задержка делает тест медленным и не гарантирует попадание точно в нужный момент.

Исторический контекст

По мере роста систем тесты стали зависеть не только от входных параметров, но и от времени, случайности, файловой системы, сети и других внешних ресурсов. Такие зависимости затрудняют воспроизводимость проверок и увеличивают стоимость диагностики отказов.

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

Постановка проблемы

В исходной функции datetime.now(...) вызывается внутри проверяемой логики. Тест не управляет этим значением: между вычислением ожидаемого результата и вызовом функции время может измениться на границе срока.

Попытка дождаться нужного момента через sleep создаёт хрупкий тест. На загруженной машине он может проснуться раньше или позже, а при параллельном запуске поведение станет ещё менее предсказуемым.

Подробное решение

Передайте функцию получения времени как зависимость:

from datetime import datetime, timezone def is_expired(expires_at, clock): return clock() >= expires_at fixed_now = datetime(2025, 3, 10, tzinfo=timezone.utc) assert is_expired(fixed_now, lambda: fixed_now) is True

В рабочем коде clock возвращает текущее время, а в тесте — заранее заданное значение. Поэтому можно отдельно проверить момент до границы, саму границу и момент после неё без ожидания и влияния системных часов.

Оператор >= здесь также является частью контракта: он означает, что срок считается истёкшим ровно в момент expires_at. Если бизнес-правило требует другой семантики, это нужно явно зафиксировать в требовании и тестах.

При внедрении времени важно использовать единый часовой пояс, обычно UTC, и сопоставимые типы дат. Подмена часов глобальным monkey patch допустима как инструмент legacy-тестирования, но обычно хуже явной зависимости: она скрывает связь функции с временем и может влиять на другие тесты.

Ситуация из практики

В сервисе подписок тесты периодически падали около полуночи: один тест создавал подписку со сроком действия «сейчас», а другой проверял её состояние после небольшой задержки. Вариант с увеличением таймаутов устранял часть сбоев, но делал набор медленнее и не решал проблему полностью.

Вариант с реальными часами и sleep был простым, но зависел от нагрузки на агент CI. Глобальная подмена системного времени давала контроль, однако могла затронуть сторонние компоненты и усложняла параллельный запуск.

Команда ввела объект или функцию Clock, передаваемую в сервис через конструктор. В боевом окружении использовались реальные часы, а в модульных тестах — фиксированные или управляемые. В результате проверки границ стали быстрыми и воспроизводимыми, а правила истечения срока можно было проверять независимо от календарного времени.

Что кандидаты часто упускают

  1. Почему передача фиксированного времени лучше проверки с задержкой?

    sleep проверяет не бизнес-границу, а поведение системы через приблизительный промежуток времени. Результат зависит от планировщика, нагрузки и точности таймера. Фиксированное время позволяет непосредственно задать условие теста и получить стабильный результат.

  2. Достаточно ли подменить часы, если даты находятся в разных часовых поясах?

    Нет. Подмена источника времени устраняет недетерминизм, но не исправляет ошибки преобразования дат. Нужно согласовать формат и часовой пояс: например, сравнивать timezone-aware значения в UTC. Отдельные тесты должны проверять преобразование пользовательского локального времени в момент, используемый бизнес-логикой.

  3. Можно ли передавать время обычным параметром вместо отдельного clock-объекта?

    Можно, если функция действительно выполняет одну операцию в переданный момент и не получает время в нескольких местах. Для сложного сервиса отдельный Clock удобнее: он централизует получение времени, позволяет управлять им в тестах и уменьшает риск, что часть кода случайно вызовет системные часы напрямую. Выбор зависит от размера компонента и количества операций со временем.