Тесты, зависящие от текущего времени, относятся к категории
нестабильных: результат может меняться в зависимости от момента запуска.
Особенно это проявляется при работе с библиотекой date-fns, где многие
функции опираются на «сегодня» или текущий timestamp через
Date.now().
Типичные проблемы при отсутствии контроля времени:
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() напрямую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 используется аналогичный механизм:
import { vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
Совместимость с date-fns сохраняется, поскольку библиотека не зависит от глобального состояния напрямую.
В средах, где используется 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();
Специализированное решение для подмены глобальной даты.
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();
});
Преимущество подхода — простота интеграции без изменения тестового раннера.
При мокировании времени особенно часто тестируются функции:
formataddDays, subDaysdifferenceInDaysisAfter, isBeforestartOfDay, 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();
});
Использование new Date() вне контекста fake timers
приводит к несогласованности.
Отсутствие восстановления оригинального времени вызывает каскадные ошибки.
date-fns работает с объектом Date, который хранит
UTC-время, но форматирование может зависеть от локали.
При использовании 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'
);
Важно учитывать:
Более устойчивый подход заключается в изоляции источника времени.
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 в следующих сценариях:
Стабильные тесты в этих областях достигаются только при строгой фиксации времени выполнения.