Мокирование текущего времени в тестах — один из ключевых приёмов
обеспечения детерминированности при работе с датами. При использовании
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.
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Альтернатива для проектов без 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.
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);
Иногда проще отказаться от «текущего времени» вообще:
const baseTime = dayjs('2024-01-01T00:00:00Z');
const result = baseTime.add(1, 'day');
Такой подход исключает зависимость от среды выполнения, но подходит только для чистых вычислений.
При фиксации времени важно учитывать плагины:
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
Фиксация времени с UTC:
jest.setSystemTime(new Date('2024-01-01T00:00:00Z'));
Day.js будет интерпретировать это время в UTC-режиме при включённом плагине.
При использовании 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()
};
}
На практике часто используется комбинация:
afterEach(() => {
jest.useRealTimers();
});
Игнорирование этого шага приводит к утечкам состояния между тестами.
setTimeout(() => {
console.log(dayjs()); // может использовать реальное время
}, 1000);
При фейковых таймерах важно контролировать исполнение через
jest.runAllTimers().
expect(dayjs().format()).toBe('...');
Такой тест всегда нестабилен без мокирования.
При работе с асинхронными функциями:
jest.useFakeTimers();
test('асинхронная логика времени', async () => {
const promise = someAsyncTimeFunction();
jest.advanceTimersByTime(5000);
await promise;
});
Наиболее устойчивые стратегии:
dayjs() в бизнес-логике