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

Тестирование кода, зависящего от текущего времени, требует детерминированности: любые вызовы системы времени должны возвращать фиксированные значения, иначе тесты становятся нестабильными. Библиотека Luxon, основанная на Date и Intl, предоставляет несколько уровней управления текущим временем, что позволяет изолировать временную зависимость как на уровне приложения, так и на уровне тестового окружения.

Источник текущего времени в Luxon

Внутренне Luxon получает текущее время через системные часы JavaScript. Основная точка входа:

DateTime.now()

Этот метод использует текущее значение времени системы через Date.now() (или эквивалентную абстракцию внутри библиотеки). Таким образом, любое использование DateTime.now() напрямую зависит от реального времени выполнения программы.

Дополнительно Luxon использует глобальную настройку:

Settings.now

Эта функция определяет источник «текущего времени» для всех операций, которые опираются на now.


Глобальное переопределение времени через Settings.now

Наиболее прямой способ мокирования времени в Luxon — переопределение Settings.now.

import { Settings } from "luxon";

Settings.now = () => 0;

После такого изменения:

DateTime.now().toMillis(); // всегда 0

Это делает все вычисления времени детерминированными, что особенно важно при тестировании:

Settings.now = () => 1672531200000; // фиксированная дата

const dt = DateTime.now();
dt.toISO(); // всегда один и тот же результат

Для корректной работы в тестовой среде важно восстанавливать оригинальное значение:

const realNow = Settings.now;

Settings.now = () => 1000;

// тесты

Settings.now = realNow;

Изоляция времени на уровне модулей

Глобальное изменение Settings.now влияет на всю среду выполнения. Это удобно для простых тестов, но становится проблемой при параллельных тестах или при наличии нескольких сценариев с разным временем.

Более безопасный подход — локализация мока через обёртку:

import { DateTime } from "luxon";

class Clock {
  now() {
    return DateTime.now();
  }
}

В тестах:

class FakeClock {
  now() {
    return DateTime.fromMillis(1672531200000);
  }
}

Использование:

function createService(clock) {
  return {
    getTimestamp() {
      return clock.now().toISO();
    }
  };
}

Такой подход устраняет зависимость от глобального состояния и позволяет подменять время точечно.


Использование fake timers (Jest, Sinon)

На уровне тестового фреймворка можно управлять системным временем без прямого вмешательства в Luxon.

Jest
jest.useFakeTimers();
jest.setSystemTime(new Date("2023-01-01T00:00:00Z"));

После этого:

DateTime.now().toISO(); // будет соответствовать установленному времени
Sinon
import sinon from "sinon";

const clock = sinon.useFakeTimers({
  now: new Date("2023-01-01T00:00:00Z").getTime()
});

После завершения теста:

clock.restore();

Эти методы работают на уровне Date, а значит автоматически влияют на Luxon без дополнительных настроек.


Взаимодействие Settings.now и системных моков

При одновременном использовании Settings.now и fake timers важно учитывать приоритет:

  • Settings.now переопределяет источник времени Luxon
  • fake timers подменяют системный Date.now()

Если одновременно заданы оба механизма, результат зависит от того, какой источник используется внутри конкретного вызова Luxon. Обычно:

  • DateTime.now() → учитывает Settings.now
  • операции с Date → зависят от fake timers

Конфликт этих уровней может приводить к расхождению времени в тестах.


Использование фиксированных DateTime вместо now

Ещё один способ исключить зависимость от текущего времени — отказ от now() в пользу явного создания даты:

const dt = DateTime.fromISO("2023-01-01T00:00:00Z");

или

DateTime.fromMillis(1672531200000);

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


Работа с часовыми поясами в моках

При фиксации времени важно учитывать влияние временных зон:

DateTime.now().setZone("Europe/Paris");

Если используется глобальный мок времени, зона не изменяет timestamp, но влияет на:

  • форматирование
  • локальное представление даты
  • вычисление смещений

Для стабильных тестов предпочтительно фиксировать не только момент времени, но и зону:

DateTime.fromMillis(1672531200000, { zone: "utc" });

Частые ошибки при мокировании времени

Некорректное управление временем приводит к трудноуловимым багам:

  • частичное восстановление Settings.now
  • смешивание fake timers и реального времени
  • использование DateTime.now() в одном модуле и Date.now() в другом
  • отсутствие фиксации временной зоны
  • кэширование результатов now() в статических переменных

Особенно проблемным становится кэширование:

const cached = DateTime.now();

function getTime() {
  return cached;
}

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


Архитектурный подход к управлению временем

В сложных системах время рассматривается как зависимость, аналогичная внешнему API или хранилищу данных. Применяется явное внедрение:

function buildService(clock) {
  return {
    isExpired(timestamp) {
      return clock.now().toMillis() > timestamp;
    }
  };
}

Это полностью устраняет необходимость глобального мокирования Luxon и делает код независимым от системного времени.


Стабильность тестов при работе с Luxon

Комбинация следующих практик обеспечивает устойчивость:

  • фиксация Settings.now на уровне тестового раннера
  • использование fake timers в Jest или Sinon
  • явное внедрение clock-абстракции
  • запрет на неявные вызовы DateTime.now() в бизнес-логике
  • контроль временных зон при форматировании и сравнении дат