Мокирование дат в тестах

Тесты, зависящие от текущего времени, относятся к категории нестабильных: результат может меняться в зависимости от момента запуска. Особенно это проявляется при работе с библиотекой date-fns, где многие функции опираются на «сегодня» или текущий timestamp через Date.now().

Типичные проблемы при отсутствии контроля времени:

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

Библиотека date-fns не содержит встроенного механизма мокирования времени, поэтому контроль текущей даты реализуется на уровне тестового окружения.


Базовый принцип контроля времени

Все стратегии мокирования времени сводятся к одному: подмена источника текущей даты.

Основные точки, через которые библиотека date-fns получает время:

  • new Date()
  • Date.now()

Любая функция date-fns, работающая с текущим моментом, в конечном итоге опирается на эти источники.


Подмена Date.now вручную

Самый простой, но ограниченный подход — локальная подмена Date.now.

const realDateNow = Date.now;

beforeEach(() => {
  Date.now = () => new Date('2024-01-01T00:00:00.000Z').getTime();
});

afterEach(() => {
  Date.now = realDateNow;
});

Применение с date-fns:

import { format } from 'date-fns';

test('форматирование фиксированной даты', () => {
  Date.now = () => new Date('2024-01-01T00:00:00Z').getTime();

  const result = format(new Date(Date.now()), 'yyyy-MM-dd');
  expect(result).toBe('2024-01-01');
});

Ограничения:

  • не контролирует new Date() напрямую
  • легко забыть восстановление оригинального значения
  • плохо масштабируется

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

Jest предоставляет полноценную систему подмены времени.

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2024-01-01T12:00:00Z'));
});

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

Пример с date-fns:

import { format, addDays } from 'date-fns';

test('добавление дней от фиксированной даты', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2024-01-01T00:00:00Z'));

  const now = new Date();
  const result = format(addDays(now, 5), 'yyyy-MM-dd');

  expect(result).toBe('2024-01-06');

  jest.useRealTimers();
});

Ключевое преимущество: подменяется вся временная система среды выполнения, включая Date, setTimeout, setInterval.


Vitest и управление временем

В Vitest используется аналогичный механизм:

import { vi } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
});

afterEach(() => {
  vi.useRealTimers();
});

Совместимость с date-fns сохраняется, поскольку библиотека не зависит от глобального состояния напрямую.


Sinon Fake Timers

В средах, где используется sinon:

import sinon from 'sinon';

const clock = sinon.useFakeTimers({
  now: new Date('2024-01-01T00:00:00Z'),
  toFake: ['Date']
});

const result = new Date().toISOString();

clock.restore();

Применение с date-fns:

import { format } from 'date-fns';

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

const result = format(new Date(), 'yyyy-MM-dd');

clock.restore();

Библиотека MockDate

Специализированное решение для подмены глобальной даты.

import MockDate from 'mockdate';

beforeEach(() => {
  MockDate.set('2024-01-01T00:00:00Z');
});

afterEach(() => {
  MockDate.reset();
});

Пример с date-fns:

import { format, subDays } from 'date-fns';
import MockDate from 'mockdate';

test('смещение назад от фиксированной даты', () => {
  MockDate.set('2024-01-10T00:00:00Z');

  const result = format(subDays(new Date(), 3), 'yyyy-MM-dd');

  expect(result).toBe('2024-01-07');

  MockDate.reset();
});

Преимущество подхода — простота интеграции без изменения тестового раннера.


Интеграция с функциями date-fns

При мокировании времени особенно часто тестируются функции:

  • format
  • addDays, subDays
  • differenceInDays
  • isAfter, isBefore
  • startOfDay, endOfDay

Пример сценария:

import { isAfter, addDays } from 'date-fns';

test('сравнение дат относительно фиксированного времени', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2024-01-01T00:00:00Z'));

  const now = new Date();
  const future = addDays(now, 1);

  const result = isAfter(future, now);

  expect(result).toBe(true);

  jest.useRealTimers();
});

Типичные ошибки при мокировании времени

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

Использование new Date() вне контекста fake timers приводит к несогласованности.

2. Утечки состояния между тестами

Отсутствие восстановления оригинального времени вызывает каскадные ошибки.

3. Игнорирование таймзоны

date-fns работает с объектом Date, который хранит UTC-время, но форматирование может зависеть от локали.


Работа с таймзонами и date-fns-tz

При использовании date-fns-tz появляется дополнительный уровень сложности: локальное отображение времени.

import { formatInTimeZone } from 'date-fns-tz';

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

const result = formatInTimeZone(
  new Date(),
  'UTC',
  'yyyy-MM-dd HH:mm:ssXXX'
);

Важно учитывать:

  • фиксация системного времени не изменяет поведение таймзон
  • необходимо отдельно контролировать временную зону окружения

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

Более устойчивый подход заключается в изоляции источника времени.

Вариант через dependency injection

export function createService(nowProvider = () => new Date()) {
  return {
    getCurrentDay() {
      return format(nowProvider(), 'yyyy-MM-dd');
    }
  };
}

Тест:

test('фиксированное время через provider', () => {
  const service = createService(() => new Date('2024-01-01T00:00:00Z'));

  expect(service.getCurrentDay()).toBe('2024-01-01');
});

Роль мокирования в устойчивости тестов

Контроль времени устраняет фактор недетерминированности, который особенно критичен при использовании date-fns в следующих сценариях:

  • расчёт сроков действия
  • календарные интерфейсы
  • планировщики задач
  • биллинг и подписки
  • логика дедлайнов

Стабильные тесты в этих областях достигаются только при строгой фиксации времени выполнения.