В JavaScript работа с текущим временем неизбежно связана с
нестабильностью тестов. Любая логика, зависящая от now(),
становится недетерминированной: один и тот же тест может проходить или
падать в зависимости от момента запуска. Особенно это критично для
доменных моделей, бизнес-правил, расчётов сроков, TTL, расписаний и
любых операций с временными окнами.
Библиотека Js-joda усиливает эту проблему тем, что по умолчанию
опирается на системное время через Instant.now() и
LocalDate.now(). Поэтому корректное тестирование требует
изоляции времени от системных часов.
Js-joda использует концепцию источника времени через
Clock. Это центральный механизм, позволяющий управлять тем,
что считается «текущим моментом».
Основные вызовы, зависящие от системного времени:
Instant.now()LocalDate.now()LocalDateTime.now()ZonedDateTime.now()Все они используют глобальные часы, если не передан альтернативный
Clock.
Ключевой компонент для тестирования — Clock.
Он позволяет:
Создание фиксированных часов выполняется через
Clock.fixed.
import { Instant, Clock, ZoneId, LocalDate } from '@js-joda/core';
const fixedInstant = Instant.parse('2025-01-01T00:00:00Z');
const clock = Clock.fixed(fixedInstant, ZoneId.UTC);
const now = Instant.now(clock);
В этом случае now всегда будет равен заданному моменту
времени независимо от системных часов.
Самый базовый сценарий — подмена текущего момента времени.
import { Instant, Clock, ZoneId } from '@js-joda/core';
const fixedClock = Clock.fixed(
Instant.parse('2024-06-10T12:00:00Z'),
ZoneId.UTC
);
const now = Instant.now(fixedClock);
Особенность подхода:
При работе с датами без времени суток используется
LocalDate. Он также может принимать Clock.
import { LocalDate, Clock, Instant, ZoneId } from '@js-joda/core';
const clock = Clock.fixed(
Instant.parse('2023-03-15T10:15:30Z'),
ZoneId.UTC
);
const date = LocalDate.now(clock);
Здесь важно понимать, что LocalDate вычисляется
относительно часового пояса, заданного в Clock.
Часовой пояс влияет на итоговое значение LocalDate и
ZonedDateTime.
import { ZonedDateTime, Clock, Instant, ZoneId } from '@js-joda/core';
const utcClock = Clock.fixed(
Instant.parse('2024-01-01T00:00:00Z'),
ZoneId.UTC
);
const mskClock = Clock.fixed(
Instant.parse('2024-01-01T00:00:00Z'),
ZoneId.of('Europe/Moscow')
);
const utcTime = ZonedDateTime.now(utcClock);
const mskTime = ZonedDateTime.now(mskClock);
Даже при одинаковом Instant результат будет различаться
из-за зоны.
Помимо фиксированного времени, часто требуется имитировать движение времени вперёд или назад.
import { Clock, Instant, ZoneId, Duration } from '@js-joda/core';
const baseClock = Clock.fixed(
Instant.parse('2024-01-01T00:00:00Z'),
ZoneId.UTC
);
const offsetClock = Clock.offset(baseClock, Duration.ofHours(5));
Теперь любой вызов now() будет возвращать время,
смещённое на 5 часов.
Для тестирования TTL, кешей и таймаутов используется последовательное
изменение Clock.
import { Clock, Instant, ZoneId, Duration } from '@js-joda/core';
const base = Clock.fixed(
Instant.parse('2024-01-01T00:00:00Z'),
ZoneId.UTC
);
const after10Seconds = Clock.offset(base, Duration.ofSeconds(10));
const after1Minute = Clock.offset(base, Duration.ofMinutes(1));
Такой подход позволяет моделировать поведение системы во времени без реального ожидания.
Наиболее стабильный подход — передача Clock через
зависимости.
Пример логики проверки срока действия:
import { Instant } from '@js-joda/core';
class TokenService {
constructor(clock) {
this.clock = clock;
}
isExpired(expirationInstant) {
return Instant.now(this.clock).isAfter(expirationInstant);
}
}
Теперь поведение класса полностью контролируется тестами.
При использовании Jest или аналогичных инструментов обычно создаётся фабрика Clock.
import { Clock, Instant, ZoneId } from '@js-joda/core';
export function createFixedClock() {
return Clock.fixed(
Instant.parse('2025-01-01T00:00:00Z'),
ZoneId.UTC
);
}
В тесте:
const clock = createFixedClock();
const service = new TokenService(clock);
Такой подход исключает необходимость глобальных моков
Date.now.
Использование системного времени напрямую внутри бизнес-логики приводит к следующим проблемам:
Прямые вызовы:
Instant.now();
LocalDate.now();
внутри логики без передачи Clock нарушают
детерминированность системы.
Для сложных сценариев календарной логики используется
ZonedDateTime с мокированным временем.
import { ZonedDateTime, Clock, Instant, ZoneId } from '@js-joda/core';
const clock = Clock.fixed(
Instant.parse('2024-10-10T10:00:00Z'),
ZoneId.UTC
);
const zdt = ZonedDateTime.now(clock);
Это позволяет тестировать:
На уровне архитектуры Clock рассматривается как
инфраструктурная зависимость. Его корректное использование
предполагает:
now()Такая структура делает временную логику прозрачной и воспроизводимой.
В сложных системах иногда требуется несколько независимых часов:
Js-joda позволяет создавать несколько Clock и передавать
их в разные подсистемы, избегая конфликтов временных контекстов.
const systemClock = Clock.systemUTC();
const testClock = Clock.fixed(Instant.parse('2024-01-01T00:00:00Z'), ZoneId.UTC);
Использование мокированного времени обеспечивает:
Временная детерминированность становится частью архитектурного контракта системы, а не побочным эффектом тестирования.