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

Мокирование текущего времени в тестах — один из ключевых приёмов обеспечения детерминированности при работе с датами. При использовании Day.js это особенно важно, поскольку любая операция без явного входного значения опирается на системное время через Date.now() и new Date().

Day.js при создании экземпляра без аргументов использует текущее системное время:

import dayjs from 'dayjs';

const now = dayjs();

Фактически это эквивалентно:

const now = dayjs(new Date());

или через внутренний вызов:

Date.now()

По этой причине любые тесты, зависящие от dayjs(), становятся нестабильными: результат меняется в зависимости от момента выполнения.


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

Типичный пример:

function isTokenExpired(expiryDate) {
  return dayjs().isAfter(dayjs(expiryDate));
}

При тестировании:

test('token should be expired', () => {
  expect(isTokenExpired('2024-01-01')).toBe(true);
});

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


Мокирование времени через Jest fake timers

Наиболее распространённый способ — использование фейковых таймеров Jest.

import dayjs from 'dayjs';

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

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

Теперь:

test('фиксированное время', () => {
  expect(dayjs().format('YYYY-MM-DD')).toBe('2024-01-01');
});

Особенности подхода

  • Перехватывает Date.now(), new Date(), setTimeout, setInterval
  • Day.js автоматически начинает использовать зафиксированное время
  • Подходит для большинства unit-тестов

Sinon fake timers

Альтернатива для проектов без Jest:

import sinon from 'sinon';
import dayjs from 'dayjs';

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

console.log(dayjs().format('YYYY-MM-DD')); // 2024-01-01

clock.restore();

Особенности

  • Гибкая настройка времени
  • Возможность «перематывать» время:
clock.tick(1000 * 60 * 60); // +1 час

Прямое переопределение Date

Более низкоуровневый способ — замена глобального Date.

const RealDate = Date;

global.Date = class extends Date {
  constructor(...args) {
    if (args.length) {
      return new RealDate(...args);
    }
    return new RealDate('2024-01-01T00:00:00Z');
  }
};

Недостатки:

  • Легко сломать сторонние библиотеки
  • Сложно поддерживать
  • Не управляет таймерами

Инкапсуляция текущего времени

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

Пример фабрики времени

import dayjs from 'dayjs';

export function createTimeProvider(now = () => dayjs()) {
  return {
    now: () => now(),
  };
}

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

function isExpired(expiry, timeProvider) {
  return timeProvider.now().isAfter(dayjs(expiry));
}

Тест:

const fixedTime = dayjs('2024-01-01T00:00:00Z');

const timeProvider = {
  now: () => fixedTime
};

expect(isExpired('2023-01-01', timeProvider)).toBe(true);

Преимущество

  • Полная независимость от глобального состояния
  • Упрощение тестирования
  • Предсказуемость логики

Фиксированное значение dayjs вместо мокирования системы

Иногда проще отказаться от «текущего времени» вообще:

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

const result = baseTime.add(1, 'day');

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


Day.js и UTC/Timezone в контексте мокирования

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

UTC

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

Фиксация времени с UTC:

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

Day.js будет интерпретировать это время в UTC-режиме при включённом плагине.


Timezone

При использовании timezone поведение может отличаться:

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

Фиксированное системное время остаётся базой, но отображение зависит от зоны:

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

Поэтому при тестировании важно фиксировать не только Date, но и ожидания формата.


Сценарии, где мокирование времени критично

Проверка истечения сроков

function isActive(start, end) {
  const now = dayjs();
  return now.isAfter(start) && now.isBefore(end);
}

Планирование задач

function canExecute(lastRun) {
  return dayjs().diff(dayjs(lastRun), 'minute') > 10;
}

Генерация временных меток

function createLog() {
  return {
    createdAt: dayjs().toISOString()
  };
}

Комбинирование подходов

На практике часто используется комбинация:

  • Jest fake timers для базовой фиксации времени
  • Инкапсуляция времени для бизнес-логики
  • Sinon для сложных сценариев с «прыжками» во времени
  • Явное использование UTC для исключения часовых зон

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

Неполное восстановление таймеров

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

Игнорирование этого шага приводит к утечкам состояния между тестами.


Смешивание real time и fake time

setTimeout(() => {
  console.log(dayjs()); // может использовать реальное время
}, 1000);

При фейковых таймерах важно контролировать исполнение через jest.runAllTimers().


Ожидания без фиксации времени

expect(dayjs().format()).toBe('...');

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


Управление временем в асинхронных сценариях

При работе с асинхронными функциями:

jest.useFakeTimers();

test('асинхронная логика времени', async () => {
  const promise = someAsyncTimeFunction();

  jest.advanceTimersByTime(5000);

  await promise;
});

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

Наиболее устойчивые стратегии:

  • фиксация системного времени через тестовый фреймворк
  • изоляция времени в отдельный слой
  • избегание прямого вызова dayjs() в бизнес-логике
  • явная работа с UTC при тестировании временных операций