Тестирование с фиксированными датами

Нестабильность времени — одна из ключевых проблем при тестировании логики, зависящей от дат. Любая операция, использующая текущее время, делает тесты недетерминированными: результат зависит от момента запуска. В контексте Day.js это особенно заметно при работе с dayjs(), форматированием относительного времени, сравнением интервалов и вычислением сроков.

Проблема «плавающего времени» в тестах

При использовании стандартного вызова:

import dayjs from 'dayjs';

const now = dayjs();

значение now постоянно меняется. Это приводит к следующим проблемам:

  • тесты дают разные результаты при каждом запуске;
  • невозможно стабильно сравнивать строки формата времени;
  • ошибки могут появляться только при определённом времени суток;
  • snapshot-тесты становятся бесполезными без фиксации времени.

Особенно критично это при проверке логики:

function isExpired(date) {
  return dayjs(date).isBefore(dayjs());
}

Без фиксации текущего времени результат зависит от момента выполнения теста.


Подход к фиксации времени через явную передачу даты

Наиболее прозрачный способ — отказ от неявного dayjs() внутри бизнес-логики.

function isExpired(date, now) {
  return dayjs(date).isBefore(now);
}

Тест:

import dayjs from 'dayjs';

const fixedNow = dayjs('2025-01-01T00:00:00Z');

test('date is expired', () => {
  const result = isExpired('2024-12-31T23:59:59Z', fixedNow);
  expect(result).toBe(true);
});

Преимущества подхода:

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

Недостаток проявляется на уровне архитектуры: требуется прокидывать текущую дату через множество функций.


Мокирование Date.now

Day.js внутри использует JavaScript Date, поэтому фиксация времени через стандартный API даёт предсказуемый результат.

const FIXED_TIME = new Date('2025-01-01T00:00:00Z').getTime();

jest.spyOn(Date, 'now').mockImplementation(() => FIXED_TIME);

После этого:

import dayjs from 'dayjs';

test('current date is stable', () => {
  expect(dayjs().format()).toBe('2025-01-01T00:00:00+00:00');
});

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

Важный момент: после теста необходимо восстанавливать поведение:

afterAll(() => {
  jest.restoreAllMocks();
});

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

Современные тестовые среды позволяют полностью контролировать системное время.

jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));

Теперь любой вызов:

dayjs();

будет возвращать фиксированную точку времени.

Пример теста:

test('formats current time', () => {
  expect(dayjs().format('YYYY-MM-DD')).toBe('2025-01-01');
});

Преимущество подхода заключается в том, что он влияет на всю систему времени, включая:

  • Date.now()
  • new Date()
  • Day.js без дополнительных настроек

Фиксация времени внутри Day.js экземпляра

Day.js позволяет создавать независимые экземпляры на основе конкретной даты:

const baseTime = dayjs('2025-01-01T00:00:00Z');

Дальнейшие операции строятся от этой точки:

baseTime.add(1, 'day').format('YYYY-MM-DD');

Тест:

test('date arithmetic is stable', () => {
  const base = dayjs('2025-01-01T00:00:00Z');

  const result = base.add(2, 'day').format('YYYY-MM-DD');

  expect(result).toBe('2025-01-03');
});

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


Тестирование относительного времени

Плагин relativeTime делает вывод зависимым от текущего момента:

import relativeTime from 'dayjs/plugin/relativeTime';
import dayjs from 'dayjs';

dayjs.extend(relativeTime);

Без фиксации времени:

dayjs('2024-12-31').fromNow();

результат будет разным в зависимости от даты запуска.

Стабилизация через fake timers

jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));

Тест:

test('relative time is stable', () => {
  const result = dayjs('2024-12-31T00:00:00Z').fromNow();
  expect(result).toBe('a day ago');
});

Работа с UTC и фиксированными датами

При использовании плагина UTC поведение также зависит от системного времени:

import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);

Фиксация:

jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));

const result = dayjs.utc().format();

Это позволяет избежать проблем с локальной зоной времени, особенно при CI-средах.


Изоляция времени через фабрику дат

Более структурный подход — создание фабрики времени:

function createTimeContext(fixedDate) {
  const base = dayjs(fixedDate);

  return {
    now: () => base,
    addDays: (n) => base.add(n, 'day'),
  };
}

Тест:

test('time context works deterministically', () => {
  const time = createTimeContext('2025-01-01');

  expect(time.addDays(5).format('YYYY-MM-DD')).toBe('2025-01-06');
});

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


Сравнение стратегий фиксации времени

Метод Изоляция Простота Гибкость
Передача now высокая средняя высокая
Mock Date.now средняя высокая средняя
Fake timers высокая высокая высокая
Day.js instance высокая высокая средняя
Фабрика времени очень высокая средняя очень высокая

Тестирование граничных дат

Фиксированное время особенно важно при проверке переходов:

const now = dayjs('2024-12-31T23:59:59Z');

const nextDay = now.add(1, 'second');

nextDay.format('YYYY-MM-DD'); // 2025-01-01

Без фиксации времени такие тесты становятся нестабильными при изменении часового пояса или времени запуска.


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

Day.js форматирование полностью зависит от внутреннего состояния даты:

const fixed = dayjs('2025-01-01T12:00:00Z');

fixed.format('YYYY-MM-DD HH:mm:ss');

В тестах это позволяет проверять:

  • локализацию форматов;
  • преобразования времени;
  • отображение пользовательских меток.

Комбинация Day.js и Jest для стабильных тестов

Наиболее устойчивый шаблон:

beforeAll(() => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
});

afterAll(() => {
  jest.useRealTimers();
});

Дальнейший код не требует модификаций и остаётся чистым:

test('stable time dependent logic', () => {
  expect(dayjs().year()).toBe(2025);
});

Особенности в асинхронных тестах

При работе с промисами важно учитывать, что время фиксируется на уровне среды:

test('async time dependent logic', async () => {
  await someAsyncFunctionUsingDayjs();

  expect(dayjs().format('YYYY')).toBe('2025');
});

Если используется реальное ожидание (setTimeout), fake timers становятся обязательными для сохранения предсказуемости.


Ошибки при фиксации времени

Частые проблемы:

  • смешивание реального и фиктивного времени;
  • использование new Date() вне контроля тестовой среды;
  • отсутствие восстановления timers;
  • использование локального времени вместо UTC в глобальных тестах.

Эти ошибки приводят к расхождениям между локальной машиной и CI.


Работа с временными зонами при фиксированных датах

При использовании dayjs/plugin/timezone:

import timezone from 'dayjs/plugin/timezone';
dayjs.extend(timezone);

Фиксация системного времени обеспечивает стабильность:

jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));

dayjs().tz('Asia/Almaty').format();

Без фиксации такие тесты могут давать разные результаты в зависимости от окружения выполнения.