Инструменты для тестирования

Код, использующий Day.js, почти всегда опирается на текущее время, таймзоны и преобразования дат. В тестовой среде это создаёт нестабильность: результат выполнения может зависеть от момента запуска теста, локали окружения или настроек системы. Для устранения таких факторов применяются инструменты фиксации времени и изоляции временных зависимостей.

Ключевая проблема заключается в том, что выражения вида dayjs() или Date.now() являются недетерминированными. Любая проверка, завязанная на текущую дату, требует стабилизации источника времени.

Фиксация системного времени

Большинство тестовых фреймворков предоставляют механизм подмены таймеров. В экосистеме JavaScript наиболее распространены Jest и Vitest, которые позволяют «замораживать» время.

При использовании Jest:

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

После этого вызов:

dayjs().format()

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

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

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

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

  • бизнес-логики с дедлайнами;
  • планировщиков задач;
  • фильтрации по диапазону дат;
  • генерации отчётов.

Контроль глобального Date

Некоторые окружения или библиотеки могут игнорировать fake timers, опираясь напрямую на Date. В таких случаях применяется явная подмена:

const RealDate = Date;

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

global.Date.now = () => new RealDate('2025-01-01T00:00:00Z').getTime();

Такой подход обеспечивает консистентность между Date и Day.js, поскольку Day.js внутри использует стандартный Date.

После тестов важно восстановление:

global.Date = RealDate;

Изоляция таймзон

Day.js использует плагины для работы с часовыми поясами, например utc и timezone. При тестировании важно учитывать, что результат форматирования может зависеть от окружения.

Подключение плагинов:

import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(timezone);

В тестовой среде фиксируется таймзона:

dayjs.tz.setDefault('UTC');

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

Тестирование иммутабельности Day.js

Одно из ключевых свойств Day.js — неизменяемость объектов. Каждый вызов метода возвращает новый экземпляр.

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

const base = dayjs('2025-01-01');

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

expect(base.format('YYYY-MM-DD')).toBe('2025-01-01');
expect(result.format('YYYY-MM-DD')).toBe('2025-01-02');

Такие тесты защищают от регрессий, связанных с мутациями, особенно при кастомных обёртках над Day.js.

Работа с моками в бизнес-логике дат

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

class TimeService {
  now() {
    return dayjs();
  }
}

В тестах этот слой заменяется:

const mockTimeService = {
  now: () => dayjs('2025-01-01T00:00:00Z')
};

Такой подход снижает связанность тестов с глобальным состоянием и исключает необходимость мокать глобальный Date.

Snapshot-тестирование форматированных дат

Day.js часто используется для генерации строковых представлений времени. В таких случаях применяются snapshot-тесты:

const formatted = dayjs('2025-01-01T12:00:00Z')
  .tz('UTC')
  .format('YYYY-MM-DD HH:mm:ss');

expect(formatted).toMatchSnapshot();

Snapshot фиксирует формат вывода, что удобно для UI-компонентов и отчётных систем. Однако при этом важно, чтобы входное время было строго детерминировано.

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

Особое внимание уделяется граничным значениям:

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

Пример проверки перехода через день:

const date = dayjs('2025-03-31T23:59:59');

const next = date.add(1, 'second');

expect(next.format('YYYY-MM-DD')).toBe('2025-04-01');

Такие сценарии часто выявляют ошибки в логике округления и форматирования.

Проверка парсинга строковых дат

Day.js поддерживает парсинг различных форматов, но в тестах важно фиксировать ожидаемое поведение:

const date = dayjs('01/02/2025', 'MM/DD/YYYY');

expect(date.format('YYYY-MM-DD')).toBe('2025-01-02');

Тесты парсинга особенно важны при работе с пользовательским вводом, где формат может быть неоднозначным.

Интеграция с Sinon и альтернативными таймерами

Помимо Jest и Vitest, часто используется Sinon для управления временем:

import sinon from 'sinon';

const clock = sinon.useFakeTimers(new Date('2025-01-01'));

dayjs().format();

clock.restore();

Sinon предоставляет более тонкий контроль над тиканием времени, включая ручное продвижение часов:

clock.tick(1000);

Это полезно при тестировании таймеров и отложенных операций.

Проверка сериализации и передачи данных

При передаче дат через API Day.js часто преобразуется в ISO-строки:

const payload = {
  date: dayjs('2025-01-01').toISOString()
};

expect(payload.date).toBe('2025-01-01T00:00:00.000Z');

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

Контроль плавающих зон времени

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

const local = dayjs('2025-01-01T00:00:00Z').local();
const utc = dayjs('2025-01-01T00:00:00Z').utc();

expect(utc.hour()).toBe(0);

Это позволяет выявлять ошибки, связанные с двойным преобразованием или неверной интерпретацией временных меток.

Изоляция тестов, зависящих от времени

Каждый тест, использующий Day.js и время, требует восстановления состояния:

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

или:

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

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

Проверка пользовательских плагинов

Day.js расширяется плагинами, которые также требуют тестирования:

dayjs.extend(customPlugin);

const result = dayjs().customMethod();

expect(result).toBeDefined();

Особое внимание уделяется корректности интеграции с core API и отсутствию конфликтов с другими расширениями.