Нестабильность времени — одна из ключевых проблем при тестировании
логики, зависящей от дат. Любая операция, использующая текущее время,
делает тесты недетерминированными: результат зависит от момента запуска.
В контексте Day.js это особенно заметно при работе с
dayjs(), форматированием относительного времени, сравнением
интервалов и вычислением сроков.
При использовании стандартного вызова:
import dayjs from 'dayjs';
const now = dayjs();
значение now постоянно меняется. Это приводит к
следующим проблемам:
Особенно критично это при проверке логики:
function isExpired(date) {
return dayjs(date).isBefore(dayjs());
}
Без фиксации текущего времени результат зависит от момента выполнения теста.
Наиболее прозрачный способ — отказ от неявного dayjs()
внутри бизнес-логики.
function isExpired(date, now) {
return dayjs(date).isBefore(now);
}
Тест:
import dayjs from 'dayjs';
const fixedNow = dayjs('2025-01-01T00:00:00Z');
test('date is expired', () => {
const result = isExpired('2024-12-31T23:59:59Z', fixedNow);
expect(result).toBe(true);
});
Преимущества подхода:
Недостаток проявляется на уровне архитектуры: требуется прокидывать текущую дату через множество функций.
Day.js внутри использует JavaScript Date, поэтому
фиксация времени через стандартный API даёт предсказуемый результат.
const FIXED_TIME = new Date('2025-01-01T00:00:00Z').getTime();
jest.spyOn(Date, 'now').mockImplementation(() => FIXED_TIME);
После этого:
import dayjs from 'dayjs';
test('current date is stable', () => {
expect(dayjs().format()).toBe('2025-01-01T00:00:00+00:00');
});
Такой подход сохраняет оригинальный код без изменений.
Важный момент: после теста необходимо восстанавливать поведение:
afterAll(() => {
jest.restoreAllMocks();
});
Современные тестовые среды позволяют полностью контролировать системное время.
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
Теперь любой вызов:
dayjs();
будет возвращать фиксированную точку времени.
Пример теста:
test('formats current time', () => {
expect(dayjs().format('YYYY-MM-DD')).toBe('2025-01-01');
});
Преимущество подхода заключается в том, что он влияет на всю систему времени, включая:
Date.now()new Date()Day.js позволяет создавать независимые экземпляры на основе конкретной даты:
const baseTime = dayjs('2025-01-01T00:00:00Z');
Дальнейшие операции строятся от этой точки:
baseTime.add(1, 'day').format('YYYY-MM-DD');
Тест:
test('date arithmetic is stable', () => {
const base = dayjs('2025-01-01T00:00:00Z');
const result = base.add(2, 'day').format('YYYY-MM-DD');
expect(result).toBe('2025-01-03');
});
Такой стиль особенно удобен при тестировании бизнес-логики расчётов дат.
Плагин relativeTime делает вывод зависимым от текущего
момента:
import relativeTime from 'dayjs/plugin/relativeTime';
import dayjs from 'dayjs';
dayjs.extend(relativeTime);
Без фиксации времени:
dayjs('2024-12-31').fromNow();
результат будет разным в зависимости от даты запуска.
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
Тест:
test('relative time is stable', () => {
const result = dayjs('2024-12-31T00:00:00Z').fromNow();
expect(result).toBe('a day ago');
});
При использовании плагина UTC поведение также зависит от системного времени:
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
Фиксация:
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
const result = dayjs.utc().format();
Это позволяет избежать проблем с локальной зоной времени, особенно при CI-средах.
Более структурный подход — создание фабрики времени:
function createTimeContext(fixedDate) {
const base = dayjs(fixedDate);
return {
now: () => base,
addDays: (n) => base.add(n, 'day'),
};
}
Тест:
test('time context works deterministically', () => {
const time = createTimeContext('2025-01-01');
expect(time.addDays(5).format('YYYY-MM-DD')).toBe('2025-01-06');
});
Такой подход убирает зависимость от глобального времени полностью.
| Метод | Изоляция | Простота | Гибкость |
|---|---|---|---|
Передача now |
высокая | средняя | высокая |
Mock Date.now |
средняя | высокая | средняя |
| Fake timers | высокая | высокая | высокая |
| Day.js instance | высокая | высокая | средняя |
| Фабрика времени | очень высокая | средняя | очень высокая |
Фиксированное время особенно важно при проверке переходов:
const now = dayjs('2024-12-31T23:59:59Z');
const nextDay = now.add(1, 'second');
nextDay.format('YYYY-MM-DD'); // 2025-01-01
Без фиксации времени такие тесты становятся нестабильными при изменении часового пояса или времени запуска.
Day.js форматирование полностью зависит от внутреннего состояния даты:
const fixed = dayjs('2025-01-01T12:00:00Z');
fixed.format('YYYY-MM-DD HH:mm:ss');
В тестах это позволяет проверять:
Наиболее устойчивый шаблон:
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
});
afterAll(() => {
jest.useRealTimers();
});
Дальнейший код не требует модификаций и остаётся чистым:
test('stable time dependent logic', () => {
expect(dayjs().year()).toBe(2025);
});
При работе с промисами важно учитывать, что время фиксируется на уровне среды:
test('async time dependent logic', async () => {
await someAsyncFunctionUsingDayjs();
expect(dayjs().format('YYYY')).toBe('2025');
});
Если используется реальное ожидание (setTimeout), fake
timers становятся обязательными для сохранения предсказуемости.
Частые проблемы:
new Date() вне контроля тестовой
среды;Эти ошибки приводят к расхождениям между локальной машиной и CI.
При использовании dayjs/plugin/timezone:
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(timezone);
Фиксация системного времени обеспечивает стабильность:
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
dayjs().tz('Asia/Almaty').format();
Без фиксации такие тесты могут давать разные результаты в зависимости от окружения выполнения.