В функции напрямую вызывается текущее время, из-за чего проверка срока действия зависит от момента запуска. Как изменить дизайн, чтобы unit-тест детерминированно проверял границу срока без Sleep и подмены глобального состояния?
package subscription
import "time"
type Subscription struct {
ExpiresAt time.Time
}
func Active(s Subscription) bool {
return time.Now().Before(s.ExpiresAt)
}
func TestActiveAtBoundary(t *testing.T) {
expires := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
_ = Active(Subscription{ExpiresAt: expires})
}
Нужно сделать текущее время явной зависимостью функции: передавать его параметром либо получать через небольшой интерфейс часов. Для unit-теста следует использовать фиксированное время, а не Sleep и не изменение глобального источника времени.
Наиболее простой вариант — передавать now time.Time, если функции нужна только одна временная точка. Интерфейс часов оправдан, когда текущее время используется во многих компонентах или требуется единый заменяемый источник.
Код, напрямую вызывающий системные часы, удобен в прикладной логике, но связывает вычисление с внешним состоянием процесса. В момент появления теста невозможно заранее гарантировать, какое значение вернёт операционная система.
Внедрение зависимостей решает эту проблему: бизнес-логика получает время от вызывающего кода, а приложение передаёт реальные часы только на границе системы. Тест передаёт контролируемое значение и проверяет именно правило, а не расписание выполнения.
time.Now() может вернуть значение до или после границы срока. Поэтому тест, который ждёт некоторое время, зависит от нагрузки на CI, планировщика горутин и точности таймера.
Дополнительная проблема — изменение глобального времени или глобальной переменной часов. Параллельные тесты могут влиять друг на друга, а порядок запуска начнёт менять результат.
В приведённом коде нельзя надёжно проверить состояние ровно в момент ExpiresAt: тест не управляет значением, с которым сравнивается срок. Более того, правило Before означает, что в сам момент истечения подписка уже неактивна.
Если зависимость используется локально, передайте время явно:
Такой вариант минимален: функция не знает, откуда взялось время, и легко проверяется на значениях до границы, в границе и после неё. Параметр now также делает временную зависимость видимой в сигнатуре.
Если компоненту требуется абстракция часов, можно определить узкий интерфейс:
В production-коде адаптер часов возвращает time.Now(), а в тесте используется FixedClock. Интерфейс не должен копировать весь API времени: узкая зависимость уменьшает объём mock-кода и связь теста с реализацией.
Важно заранее зафиксировать семантику границы. Before делает срок исключающим: при now == ExpiresAt результат false. Если срок должен включать момент окончания, потребуется другое правило, например сравнение через !now.After(expiresAt).
Не следует внедрять часы только ради механического покрытия. Для простой чистой функции передача time.Time обычно понятнее интерфейса. Интерфейс полезен, когда источник времени является зависимостью нескольких операций или его нужно централизованно контролировать.
Сервис выдавал временные токены и проверял их действительность через time.Now(). Первоначальный тест использовал Sleep, поэтому периодически падал на загруженных CI-агентах: задержка не гарантировала, что планировщик предоставит процессору тесту именно нужный момент.
Рассматривались три варианта. Увеличить Sleep было проще, но это замедляло тест и не устраняло гонку с планировщиком. Изменять глобальную переменную времени было быстрее для внедрения, но создавало риск утечки состояния между параллельными тестами. Передать время в функцию проверки потребовало небольшого изменения API, зато сделало правило детерминированным.
Выбрали третий вариант для чистой проверки срока, а получение time.Now() оставили на уровне обработчика запроса. В результате тесты проверяли значения до, на границе и после срока без ожидания, а production-поведение осталось прежним.
time.Now() на переменную-функцию, например var now = time.Now?Технически это позволяет присвоить тестовую функцию, но создаёт изменяемое глобальное состояние. При параллельном выполнении тестов такая подмена может быть небезопасной и привести к влиянию одного теста на другой. Для небольшого непараллельного legacy-теста это временный компромисс, но предпочтительнее явная передача времени или экземпляра часов.
Sleep эквивалентом проверки времени?Sleep гарантирует только минимальную задержку, но не точное значение time.Now(). После пробуждения процесс может быть вытеснен, а системные часы могут иметь другую точность или корректироваться. Такой тест проверяет поведение планировщика и среды выполнения вместе с бизнес-правилом, поэтому становится медленным и нестабильным.
time.Time?Для проверки обычных календарных сроков time.Time достаточно. Для измерения прошедшей длительности лучше абстрагировать операцию получения времени или таймера так, чтобы тест мог продвинуть виртуальные часы без ожидания. Важно не смешивать календарное сравнение с измерением интервала: эти сценарии могут иметь разные требования к источнику времени и его монотонной составляющей.