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

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

Это приводит к:

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

Ключевая задача тестирования date-fns-кода заключается в создании контролируемого времени выполнения, при котором результат вычислений всегда воспроизводим.


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

Использование фейковых таймеров

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

Jest

import { format } from 'date-fns';

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

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

test('format date should be stable', () => {
  const result = format(new Date(), 'yyyy-MM-dd');
  expect(result).toBe('2024-01-01');
});

Фиксация времени через jest.setSystemTime делает любые вызовы new Date() предсказуемыми.


Vitest

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

import { vi } from 'vitest';
import { format } from 'date-fns';

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

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

test('format stable date output', () => {
  expect(format(new Date(), 'dd.MM.yyyy')).toBe('10.06.2024');
});

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

Функции форматирования являются наиболее чувствительной частью date-fns, так как они зависят от локали, таймзоны и входного объекта Date.

Базовая проверка format

import { format } from 'date-fns';

test('formats ISO date correctly', () => {
  const date = new Date('2023-12-25T00:00:00Z');
  expect(format(date, 'yyyy/MM/dd')).toBe('2023/12/25');
});

Проверка локализованных форматов

import { format } from 'date-fns';
import { ru } from 'date-fns/locale';

test('formats date with locale', () => {
  const date = new Date('2023-12-25T00:00:00Z');
  const result = format(date, 'PPP', { locale: ru });

  expect(result).toContain('декабря');
});

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


Проверка арифметики дат

date-fns широко используется для вычислений над датами, и тесты здесь должны учитывать крайние случаи.

differenceInDays

import { differenceInDays } from 'date-fns';

test('difference in days', () => {
  const a = new Date('2024-01-10');
  const b = new Date('2024-01-01');

  expect(differenceInDays(a, b)).toBe(9);
});

add / sub функции

import { addMonths } from 'date-fns';

test('add months correctly handles overflow', () => {
  const date = new Date('2024-01-31');
  const result = addMonths(date, 1);

  expect(result.getMonth()).toBe(1);
});

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

  • переход 31 → 28/29 февраля
  • високосные годы
  • переходы через год
  • отрицательные значения смещения

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

Граничные случаи являются источником большинства ошибок.

Переход через полночь

import { startOfDay } from 'date-fns';

test('startOfDay resets time correctly', () => {
  const date = new Date('2024-05-10T18:45:00Z');
  const result = startOfDay(date);

  expect(result.toISOString()).toBe('2024-05-10T00:00:00.000Z');
});

Конец месяца

import { endOfMonth } from 'date-fns';

test('endOfMonth returns last day', () => {
  const date = new Date('2024-02-10');
  const result = endOfMonth(date);

  expect(result.getDate()).toBe(29); // 2024 leap year
});

Работа с таймзонами в тестах

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

При тестировании важно различать:

  • локальное время окружения
  • UTC-время
  • пользовательскую таймзону

Использование date-fns-tz

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

test('timezone formatting', () => {
  const date = new Date('2024-01-01T00:00:00Z');
  const result = format(date, 'yyyy-MM-dd HH:mm', {
    timeZone: 'Europe/Berlin'
  });

  expect(result).toBe('2024-01-01 01:00');
});

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


Мокирование Date и изоляция времени

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

Полная изоляция времени

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-03-15T10:00:00Z'));
});

Это гарантирует:

  • стабильность вычислений
  • повторяемость сценариев
  • контроль над «текущей датой»

Тестирование функций округления времени

Функции вроде roundToNearestMinutes требуют проверки множества сценариев.

import { roundToNearestMinutes } from 'date-fns';

test('rounds correctly down', () => {
  const date = new Date('2024-01-01T10:14:00Z');

  const result = roundToNearestMinutes(date, { nearestTo: 15 });

  expect(result.getMinutes()).toBe(15);
});

Особое внимание:

  • ровно середина интервала
  • поведение при нестандартных шагах (7, 20, 45 минут)
  • переход через часы

Проверка иммутабельности данных

date-fns придерживается принципа immutable API, где входной объект Date не изменяется.

import { addDays } from 'date-fns';

test('does not mutate input date', () => {
  const date = new Date('2024-01-01');
  const copy = new Date(date);

  addDays(date, 5);

  expect(date).toEqual(copy);
});

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


Комбинированные сценарии вычислений

В реальных системах функции date-fns редко используются изолированно. Чаще они комбинируются.

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

test('complex date pipeline', () => {
  const base = new Date('2024-01-01T12:30:00Z');

  const result = format(
    startOfDay(addDays(base, 3)),
    'yyyy-MM-dd'
  );

  expect(result).toBe('2024-01-04');
});

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


Проверка регрессионных сценариев

Регрессионное тестирование в контексте date-fns фокусируется на:

  • изменениях поведения при обновлении версии библиотеки
  • изменениях в локалях
  • корректности обработки крайних дат
test('regression: leap year handling', () => {
  const date = new Date('2020-02-29');
  const result = addYears(date, 1);

  expect(result.getMonth()).toBe(1);
  expect(result.getDate()).toBe(28);
});

Тестирование парсинга строк дат

import { parseISO } from 'date-fns';

test('parse ISO string', () => {
  const date = parseISO('2024-07-01T00:00:00Z');

  expect(date.getUTCFullYear()).toBe(2024);
});

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


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

Ключевое требование к тестам date-fns-кода — полная воспроизводимость:

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

Эти принципы формируют основу стабильного тестирования любой логики, построенной на date-fns.