Fixed clock для детерминированных тестов

Проблема недетерминированного времени

Работа с текущим временем в тестах создаёт фундаментальную нестабильность: результат выполнения зависит от момента запуска, что приводит к «плавающим» тестам. Любые конструкции вида:

LocalDate.now()
Instant.now()
ZonedDateTime.now()

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

Основные последствия:

  • невозможность воспроизвести падения тестов
  • зависимость от часового пояса машины
  • сложность проверки граничных случаев (конец месяца, високосные годы)
  • нестабильность CI-сборок

Библиотека js-joda решает эту проблему через абстракцию Clock, позволяющую подменять источник времени.


Абстракция Clock

В основе механизма лежит концепция часов как отдельного объекта, а не глобального состояния системы.

Clock в js-joda:

  • инкапсулирует получение текущего времени
  • позволяет подменять поведение в тестах
  • обеспечивает единый источник времени для всех temporal-операций

Основные фабрики:

  • Clock.systemUTC()
  • Clock.system(ZoneId)
  • Clock.fixed(Instant, ZoneId)
  • Clock.offset(baseClock, Duration)

Fixed Clock: фиксированное время

Clock.fixed создаёт неизменяемые часы, возвращающие одно и то же значение времени независимо от вызовов.

Сигнатура:

Clock.fixed(instant, zone)

Параметры:

  • instant — точка времени (Instant)
  • zone — часовой пояс (ZoneId)

Базовый пример

import { Clock, Instant, ZoneId, LocalDate } from '@js-joda/core';

const fixedInstant = Instant.parse('2024-01-01T00:00:00Z');
const clock = Clock.fixed(fixedInstant, ZoneId.UTC);

const date = LocalDate.now(clock);

console.log(date.toString()); // 2024-01-01

Результат полностью детерминирован: независимо от момента запуска теста, дата всегда одинакова.


Интеграция с бизнес-логикой

Ключевая практика — передача Clock через зависимости, а не использование now() напрямую.

Антипаттерн

class OrderService {
  createOrder() {
    return {
      createdAt: Instant.now()
    };
  }
}

Проблема — невозможность контроля времени.


Правильный подход

import { Instant } from '@js-joda/core';

class OrderService {
  constructor(clock) {
    this.clock = clock;
  }

  createOrder() {
    return {
      createdAt: Instant.now(this.clock)
    };
  }
}

Тест с Fixed Clock

import { Clock, Instant, ZoneId } from '@js-joda/core';

test('order has deterministic creation time', () => {
  const fixedClock = Clock.fixed(
    Instant.parse('2025-06-01T12:00:00Z'),
    ZoneId.UTC
  );

  const service = new OrderService(fixedClock);

  const order = service.createOrder();

  expect(order.createdAt.toString())
    .toBe('2025-06-01T12:00:00Z');
});

Поведение ZonedTime и Fixed Clock

При использовании временных зон важно учитывать, что Clock.fixed фиксирует:

  • момент времени (Instant)
  • и контекст зоны (ZoneId)
import { ZonedDateTime, Clock, Instant, ZoneId } from '@js-joda/core';

const clock = Clock.fixed(
  Instant.parse('2024-03-10T10:00:00Z'),
  ZoneId.of('Europe/Paris')
);

const zdt = ZonedDateTime.now(clock);

Результат зависит от зоны, но момент времени остаётся неизменным.


Использование в граничных тестах

Fixed Clock особенно полезен для сценариев:

Конец месяца

const clock = Clock.fixed(
  Instant.parse('2024-01-31T23:59:59Z'),
  ZoneId.UTC
);

Високосный год

const clock = Clock.fixed(
  Instant.parse('2024-02-29T10:00:00Z'),
  ZoneId.UTC
);

Переход на летнее время

const clock = Clock.fixed(
  Instant.parse('2024-03-31T01:30:00Z'),
  ZoneId.of('Europe/Berlin')
);

Fixed Clock vs System Clock

Характеристика System Clock Fixed Clock
Изменяется со временем Да Нет
Подходит для production Да Нет
Подходит для тестов Нет Да
Повторяемость Нет Да
Воспроизводимость багов Сложная Простая

Использование в сервисах с временной логикой

Таймауты и дедлайны

class TokenService {
  constructor(clock) {
    this.clock = clock;
  }

  isExpired(expirationInstant) {
    const now = Instant.now(this.clock);
    return now.isAfter(expirationInstant);
  }
}

Комбинация Fixed Clock и Offset Clock

Иногда требуется моделировать «время после события».

import { Clock, Duration } from '@js-joda/core';

const baseClock = Clock.fixed(
  Instant.parse('2024-01-01T00:00:00Z'),
  ZoneId.UTC
);

const futureClock = Clock.offset(baseClock, Duration.ofHours(5));

Поведение:

  • baseClock — фиксированная точка
  • futureClock — сдвинутое время

Типичные ошибки при использовании Fixed Clock

1. Смешивание системного и фиксированного времени

Instant.now(); // плохо в тестируемом коде
Instant.now(clock); // правильно

2. Хардкод времени внутри бизнес-логики

const now = Instant.parse('2024-01-01T00:00:00Z');

Такой подход ломает гибкость тестов.


3. Игнорирование ZoneId

Clock.fixed(instant, ZoneId.UTC);

При тестировании локализованного поведения зона должна быть частью сценария.


Архитектурный подход: Clock как зависимость уровня приложения

Оптимальная структура:

  • Clock создаётся на уровне composition root
  • передаётся во все сервисы
  • в тестах заменяется на Fixed Clock
  • в production используется system clock
const appClock = Clock.systemUTC();

const service = new OrderService(appClock);

Повышение стабильности тестового набора

Использование Fixed Clock позволяет:

  • убрать flakiness тестов
  • ускорить выполнение (нет ожиданий времени)
  • гарантировать воспроизводимость CI
  • упростить проверку временных сценариев

Расширенные сценарии моделирования времени

Симуляция длительного периода

const startClock = Clock.fixed(
  Instant.parse('2024-01-01T00:00:00Z'),
  ZoneId.UTC
);

const dayLater = Clock.offset(startClock, Duration.ofDays(1));

Прогон бизнес-процессов

  • создание события
  • смещение времени
  • проверка состояния системы

Влияние на доменную модель

Использование Fixed Clock способствует:

  • чистой архитектуре
  • отделению времени от логики
  • улучшению тестируемости
  • снижению скрытых зависимостей

Итоговое поведение модели времени

Вся система времени в js-joda может быть сведена к одному принципу:

  • время либо «текущее» (system clock)
  • либо «контролируемое» (fixed/offset clock)

Fixed Clock превращает временную ось из внешнего фактора в управляемую переменную тестовой среды, обеспечивая полную детерминированность вычислений над temporal-объектами.