Стратегии тестирования

Тестирование кода, использующего js-joda, неизбежно упирается в проблему недетерминированного времени. Любая зависимость от текущего момента приводит к нестабильным тестам, которые ломаются при смене даты, часового пояса или системного времени.

Основной принцип — устранение прямых вызовов системного времени через now() без контекста и замена их управляемым источником времени.

В js-joda ключевую роль играет Clock, позволяющий фиксировать или контролировать текущий момент:

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

const zone = ZoneId.of('Europe/Moscow');
const fixedInstant = Instant.parse('2025-01-01T00:00:00Z');

const clock = Clock.fixed(fixedInstant, zone);

const now = Instant.now(clock);

Использование фиксированного Clock обеспечивает полную воспроизводимость поведения временных вычислений. Любые вызовы LocalDate.now(clock), ZonedDateTime.now(clock) или Instant.now(clock) становятся детерминированными.


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

Код, использующий js-joda, должен проектироваться так, чтобы источник времени передавался извне. Это позволяет тестировать логику без глобальных моков и без изменения окружения выполнения.

Типовая стратегия — инъекция Clock в сервисный слой:

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

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

  isExpired(expirationDate) {
    const today = LocalDate.now(this.clock);
    return today.isAfter(expirationDate);
  }
}

Тестирование такой логики сводится к управлению Clock, а не к подмене глобального состояния.


Тестирование граничных дат и календарных эффектов

Временная логика часто ломается на границах календаря: конец месяца, високосные годы, смена года.

Особое внимание требуется к операциям:

  • plusDays, minusDays
  • plusMonths, minusMonths
  • withDayOfMonth
  • withMonth

Пример проверки конца месяца:

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

const date = LocalDate.of(2024, 1, 31);
const result = date.plusMonths(1);

result.toString(); // 2024-02-29

Тесты должны фиксировать ожидания на уровне календарной семантики, а не арифметики дней.

Особая зона риска — февраль в високосные годы. Проверки должны включать минимум два сценария: високосный и невисокосный год.


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

Использование ZonedDateTime и ZoneId требует проверки поведения в условиях перехода на летнее и зимнее время.

import { ZonedDateTime, ZoneId, LocalDateTime } from '@js-joda/core';

const zone = ZoneId.of('Europe/Berlin');
const local = LocalDateTime.of(2024, 3, 31, 2, 30);

const zdt = ZonedDateTime.of(local, zone);

В периоды DST некоторые локальные времена не существуют или дублируются. Тестирование должно учитывать:

  • пропущенные локальные часы (gap)
  • повторяющиеся часы (overlap)

Стратегия тестирования заключается в подборе фиксированных локальных значений вокруг перехода DST и проверке фактического смещения offset.


Проверка парсинга и форматирования

Одним из источников ошибок становится преобразование строк ISO-8601 и обратно. js-joda строго следует ISO-стандартам, но тестирование необходимо для обеспечения совместимости с внешними системами.

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

const input = '2025-06-15T10:20:30';
const parsed = LocalDateTime.parse(input);

parsed.toString(); // 2025-06-15T10:20:30

Стратегии тестирования:

  • round-trip проверка (parse → format → parse)
  • сравнение с эталонными ISO-строками
  • проверка поведения при отсутствии временной зоны

Изоляция вычислений времени от системного Date

Несмотря на наличие js-joda, в проектах часто сохраняется смешение с Date. Это создаёт скрытые дефекты, особенно при конвертации временных зон.

Стратегия тестирования включает:

  • запрет прямого использования new Date() в бизнес-логике
  • контроль точек конвертации Instant <-> Date
  • тестирование потерь точности
import { Instant } from '@js-joda/core';

const instant = Instant.parse('2025-01-01T12:00:00Z');
const date = new Date(instant.toEpochMilli());

const back = Instant.ofEpochMilli(date.getTime());

Тесты должны проверять эквивалентность после преобразования.


Фиксация времени через Clock.fixed

Clock.fixed является ключевым инструментом стабильного тестирования временных сценариев. Он устраняет зависимость от реального времени выполнения.

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

const clock = Clock.fixed(
  Instant.parse('2025-03-10T00:00:00Z'),
  ZoneId.UTC
);

const date = LocalDate.now(clock);

Стратегия использования:

  • один фиксированный Clock на тест
  • отдельный Clock для каждого сценария времени
  • запрет смешивания реального и фиксированного времени в одном тесте

Тестирование интервалов и длительностей

Операции с Duration и Period требуют проверки арифметики времени на разных уровнях:

  • секундные интервалы (Duration)
  • календарные периоды (Period)
import { Duration } from '@js-joda/core';

const d = Duration.ofHours(1).plusMinutes(30);
d.toMinutes(); // 90

Особое внимание уделяется:

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

Проверка сравнений и сортировки временных объектов

Сравнение временных сущностей должно быть детерминированным и независимым от строкового представления.

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

const a = Instant.parse('2025-01-01T10:00:00Z');
const b = Instant.parse('2025-01-01T11:00:00Z');

a.isBefore(b); // true

Стратегия тестирования:

  • проверка всех операторов сравнения (isBefore, isAfter, isEqual)
  • сортировка массивов временных объектов
  • проверка стабильности порядка при одинаковых значениях

Property-based тестирование временных операций

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

  • случайные даты в диапазоне
  • случайные временные зоны
  • случайные операции (plusDays, minusMonths)

Цель — выявление инвариантов:

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

Тестирование сериализации в доменных моделях

При использовании js-joda в доменных слоях важно проверять корректность сериализации объектов:

  • JSON-представление
  • восстановление из DTO
  • отсутствие потери зоны или точности
const dto = {
  date: '2025-01-01'
};

const parsed = LocalDate.parse(dto.date);

Стратегия:

  • единый формат сериализации
  • явное тестирование контрактов API
  • исключение неявных преобразований через Date

Контроль границ времени в интеграционных сценариях

Интеграционные тесты должны учитывать взаимодействие нескольких временных объектов:

  • Instant как универсальная точка времени
  • ZonedDateTime как пользовательское представление
  • LocalDate как бизнес-дата без зоны

Типовой сценарий:

  • получение времени в UTC
  • преобразование в локальную зону
  • сохранение в хранилище
  • восстановление и обратное преобразование

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