Мокирование текущего времени

В JavaScript работа с текущим временем неизбежно связана с нестабильностью тестов. Любая логика, зависящая от now(), становится недетерминированной: один и тот же тест может проходить или падать в зависимости от момента запуска. Особенно это критично для доменных моделей, бизнес-правил, расчётов сроков, TTL, расписаний и любых операций с временными окнами.

Библиотека Js-joda усиливает эту проблему тем, что по умолчанию опирается на системное время через Instant.now() и LocalDate.now(). Поэтому корректное тестирование требует изоляции времени от системных часов.


Источники текущего времени в Js-joda

Js-joda использует концепцию источника времени через Clock. Это центральный механизм, позволяющий управлять тем, что считается «текущим моментом».

Основные вызовы, зависящие от системного времени:

  • Instant.now()
  • LocalDate.now()
  • LocalDateTime.now()
  • ZonedDateTime.now()

Все они используют глобальные часы, если не передан альтернативный Clock.


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 всегда будет равен заданному моменту времени независимо от системных часов.


Мокирование Instant.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.now

При работе с датами без времени суток используется 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 результат будет различаться из-за зоны.


Использование Clock.offset для динамического сдвига времени

Помимо фиксированного времени, часто требуется имитировать движение времени вперёд или назад.

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 в доменную модель

Наиболее стабильный подход — передача 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 нарушают детерминированность системы.


Комбинирование Clock с ZonedDateTime

Для сложных сценариев календарной логики используется 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);

Стабильность временных вычислений

Использование мокированного времени обеспечивает:

  • воспроизводимость сценариев
  • точное моделирование временных границ
  • контроль над граничными условиями (начало/конец дня, месяца, года)
  • отсутствие зависимости от локальной среды выполнения

Временная детерминированность становится частью архитектурного контракта системы, а не побочным эффектом тестирования.