Короткий ответ
Нужно сделать источник времени подменяемым — через внедрение зависимости (передавать clock/now как параметр) или через библиотеку подмены времени (например, freezegun), чтобы в тесте зафиксировать конкретный момент времени вместо реального datetime.now().
Как это работает подробнее
freezegun подменяет вызовы datetime.now()/date.today() на уровне интерпретатора на время теста, фиксируя указанную дату для всего кода, выполняемого внутри декоратора или контекстного менеджера. Альтернативный, более явный подход — архитектурно внедрять функцию или объект clock в код вместо прямого вызова datetime.now(), что не требует монкипатчинга стандартной библиотеки и прозрачнее в чтении.
Пример кода
testing_example_309.py
from freezegun import freeze_time
from datetime import date
def days_until_expiry(expiry_date: date) -> int:
return (expiry_date - date.today()).days
@freeze_time("2024-01-01")
def test_days_until_expiry():
assert days_until_expiry(date(2024, 1, 10)) == 9Что использовать на практике
- Практика. Выбор между freezegun и внедрением зависимости clock зависит от размера кодовой базы и готовности к рефакторингу: freezegun быстрее внедрить в уже существующий код без изменения сигнатур функций, явный clock даёт более предсказуемое и read-only поведение без побочных эффектов монкипатчинга на весь процесс выполнения теста.
Подводные камни
- Риск. Писать тесты, которые неявно зависят от реального текущего времени (например,
assert created_at.date() == date.today()), что иногда ломается в полночь или при разнице часовых поясов CI-машины. Не учитывать часовой пояс при подмене времени, из-за чего тест проходит локально, но падает в CI с другим TZ.