Моки для дат и времени

Без фиксации текущего времени тесты с timeago.js недетерминированы: результат format(date) зависит от того, когда запускается тест. Моки времени решают эту проблему.


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

it('показывает "2 дня назад"', () => {
  const date = new Date(Date.now() - 2 * 86400 * 1000);
  expect(format(date, 'ru')).toBe('2 дня назад'); // Нестабильно!
});

На следующий день Date.now() сместится, и вычисленная дата станет “3 дня назад”. Тест упадёт без причины.


jest.useFakeTimers и jest.setSystemTime

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

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(FIXED_TIME);
});

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

it('показывает относительную дату', () => {
  const twoDaysAgo = new Date(FIXED_TIME - 2 * 86400_000);
  expect(format(twoDaysAgo, 'ru')).toBe('2 дня назад');
});

Vitest: фиксация времени

import { vi, beforeEach, afterEach } from 'vitest';
import { format } from 'timeago.js';

const FIXED = new Date('2025-06-01T12:00:00Z');

beforeEach(() => {
  vi.useFakeTimers();
  vi.setSystemTime(FIXED);
});

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

it('форматирует корректно при фиксированном времени', () => {
  const date = new Date(FIXED.getTime() - 3600_000); // 1 час назад
  expect(format(date, 'ru')).toMatch(/час/);
});

Мок Date глобально

Для сред без встроенной поддержки fake timers:

const OriginalDate = Date;
const FIXED = 1748779200000; // 2025-06-01T12:00:00Z

beforeAll(() => {
  global.Date = class extends OriginalDate {
    constructor(...args: any[]) {
      if (args.length === 0) {
        super(FIXED);
      } else {
        super(...args);
      }
    }
    static now() { return FIXED; }
  } as DateConstructor;
});

afterAll(() => {
  global.Date = OriginalDate;
});

Мок самой библиотеки

Когда нужно изолировать тест от реализации timeago.js:

jest.mock('timeago.js', () => ({
  format:   jest.fn(() => '2 часа назад'),
  render:   jest.fn(),
  cancel:   jest.fn(),
  register: jest.fn(),
}));

import { format } from 'timeago.js';

it('компонент использует format', () => {
  render(<TimeAgo date={new Date()} />);
  expect(format).toHaveBeenCalledTimes(1);
});

Частичный мок: только format

jest.mock('timeago.js', () => ({
  ...jest.requireActual('timeago.js'),
  format: jest.fn(() => '5 минут назад'),
}));

Остальные функции (render, cancel, register) работают по-настоящему, только format замокана.


Контролируемые значения мока

const mockFormat = jest.fn();
jest.mock('timeago.js', () => ({ format: mockFormat }));

describe('с разными возвращаемыми значениями', () => {
  it('отображает первое значение', () => {
    mockFormat.mockReturnValue('только что');
    render(<TimeAgo date={new Date()} />);
    expect(screen.getByText('только что')).toBeInTheDocument();
  });

  it('обновляется при смене значения', () => {
    mockFormat
      .mockReturnValueOnce('только что')
      .mockReturnValueOnce('1 минуту назад');
    // Первый рендер — 'только что'
    // После обновления — '1 минуту назад'
  });
});

Мок relativeDate в FormatOptions

import { format } from 'timeago.js';

const FIXED_NOW = new Date('2025-06-01T12:00:00Z');

it('format с relativeDate', () => {
  const result = format('2025-06-01T11:00:00Z', 'ru', {
    relativeDate: FIXED_NOW,
  });
  expect(result).toMatch(/час/);
});

relativeDate — встроенная опция timeago.js. Позволяет указать точку отсчёта явно, без подмены Date.now.


Шаблон фабрики дат для тестов

// test/factories/dates.ts
const BASE = new Date('2025-06-01T12:00:00Z');

export const dates = {
  now:          () => new Date(BASE),
  minutesAgo:   (n: number) => new Date(BASE.getTime() - n * 60_000),
  hoursAgo:     (n: number) => new Date(BASE.getTime() - n * 3600_000),
  daysAgo:      (n: number) => new Date(BASE.getTime() - n * 86400_000),
  minutesLater: (n: number) => new Date(BASE.getTime() + n * 60_000),
};

// Использование в тестах
it('показывает "5 минут назад"', () => {
  jest.setSystemTime(BASE);
  expect(format(dates.minutesAgo(5), 'ru')).toMatch(/минут/);
});

Мок таймеров setInterval

it('хук обновляет значение по интервалу', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-06-01T12:00:00Z'));

  const { result } = renderHook(() =>
    useTimeAgo(new Date('2025-06-01T11:59:00Z')) // 1 минуту назад
  );

  expect(result.current).toMatch(/минут/);

  // Перемотать на 2 минуты вперёд
  act(() => { jest.advanceTimersByTime(120_000); });

  // Значение должно остаться корректным
  expect(typeof result.current).toBe('string');

  jest.useRealTimers();
});

Стратегия выбора подхода к мокам

Сценарий Подход
Тест значения format jest.setSystemTime + реальный format
Тест компонента без зависимости от библиотеки Полный мок timeago.js
Тест поведения с разными выводами mockReturnValueOnce
Тест с явной точкой отсчёта relativeDate в FormatOptions
Тест таймеров и интервалов jest.useFakeTimers + advanceTimersByTime