Тестирование часовых поясов

Работа с временем в тестах требует строгой детерминированности: любые зависимости от системного времени, локали или часового пояса способны приводить к нестабильным результатам. В контексте Day.js это особенно важно, поскольку библиотека активно использует системные настройки окружения, если явно не заданы правила преобразования времени.

Day.js сам по себе не хранит состояние времени, но при подключении плагинов utc и timezone поведение начинает зависеть от IANA-зон и конфигурации среды выполнения.


Базовая настройка окружения для тестов

Перед тестированием временных сценариев необходимо зафиксировать среду выполнения. Основные источники нестабильности:

  • системный часовой пояс (process.env.TZ)
  • локальное системное время
  • поведение Node.js и браузера
  • различия CI и локальной машины

Для Node.js ключевым инструментом является переменная окружения:

TZ=UTC jest

или в конфигурации npm-скрипта:

{
  "scripts": {
    "test": "TZ=UTC jest"
  }
}

Это позволяет унифицировать поведение Date и Day.js в разных окружениях.


Подключение UTC и Timezone плагинов

Для корректной работы с часовыми поясами в Day.js используются два основных плагина:

import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'

dayjs.extend(utc)
dayjs.extend(timezone)

Плагин utc обеспечивает нормализацию времени, а timezone добавляет поддержку IANA-зон и методов tz.


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

Для предсказуемых тестов важно контролировать текущее время. В Jest и аналогичных фреймворках используется подмена системного времени.

Jest Fake Timers

beforeAll(() => {
  jest.useFakeTimers()
  jest.setSystemTime(new Date('2025-01-01T12:00:00Z'))
})

afterAll(() => {
  jest.useRealTimers()
})

После этого все вызовы dayjs() будут возвращать фиксированную точку отсчёта.


Тестирование преобразования временных зон

Основной сценарий — проверка конвертации времени между зонами.

const date = dayjs.tz('2025-01-01 12:00', 'UTC')

const tokyo = date.tz('Asia/Tokyo')
const berlin = date.tz('Europe/Berlin')

expect(tokyo.format()).toBe('2025-01-01T21:00:00+09:00')
expect(berlin.format()).toBe('2025-01-01T13:00:00+01:00')

Здесь важно понимать, что Day.js хранит момент времени в UTC, а преобразование зоны влияет только на отображение.


Проверка локального времени и системного часового пояса

При отсутствии явного tz Day.js использует системный часовой пояс:

const local = dayjs('2025-01-01T12:00:00')

console.log(local.format())

Для тестов такое поведение нежелательно, поэтому фиксируют окружение:

process.env.TZ = 'UTC'

Альтернативно можно принудительно задавать зону:

dayjs.tz.setDefault('UTC')

Работа с DST (летнее/зимнее время)

Одной из сложных проблем является переход на летнее время. Day.js корректно учитывает DST через IANA-базы, но тесты должны явно фиксировать критические даты.

Пример проверки перехода:

const beforeDST = dayjs.tz('2025-03-30 01:30', 'Europe/Berlin')
const afterDST = dayjs.tz('2025-03-30 03:30', 'Europe/Berlin')

expect(afterDST.diff(beforeDST, 'hour')).toBe(1)

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


Изоляция тестов при работе с timezone

При параллельном запуске тестов возможны конфликты глобальных настроек Day.js. Особенно это касается:

  • dayjs.tz.setDefault
  • process.env.TZ
  • глобального расширения плагинов

Рекомендуется изолировать изменения:

const originalTZ = process.env.TZ

beforeEach(() => {
  process.env.TZ = 'UTC'
})

afterEach(() => {
  process.env.TZ = originalTZ
})

Сравнение времён в тестах

Day.js не рекомендуется сравнивать напрямую через строки формата. Вместо этого используется сравнение Unix timestamp:

const a = dayjs.tz('2025-01-01 00:00', 'UTC')
const b = dayjs.tz('2025-01-01 03:00', 'Europe/Moscow')

expect(a.valueOf()).toBe(b.valueOf())

Это гарантирует, что сравнение не зависит от формата вывода.


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

Форматирование зависит от активной зоны:

const date = dayjs.tz('2025-01-01 12:00', 'UTC')

expect(date.tz('Asia/Tokyo').format('YYYY-MM-DD HH:mm'))
  .toBe('2025-01-01 21:00')

expect(date.tz('America/New_York').format('YYYY-MM-DD HH:mm'))
  .toBe('2025-01-01 07:00')

При этом важно избегать смешивания локального dayjs() и dayjs.tz() в одном тесте без явной необходимости.


Использование snapshot-тестирования

Snapshot-тесты полезны для фиксации сложных временных сценариев:

const result = dayjs.tz('2025-06-01 10:00', 'Europe/Paris')
  .tz('Asia/Dubai')
  .format()

expect(result).toMatchSnapshot()

Однако такие тесты чувствительны к изменению IANA-баз, поэтому их следует использовать только для стабильных зон.


Типичные ошибки при тестировании часовых поясов

Частые проблемы:

  • смешивание UTC и локального времени без явной конвертации
  • отсутствие фиксации process.env.TZ
  • использование Date.now() без мокирования
  • сравнение строк формата вместо timestamp
  • игнорирование DST-переходов
  • отсутствие явного подключения utc и timezone

Изоляция Day.js в тестовой среде

Иногда требуется создать полностью изолированную конфигурацию Day.js:

import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'

const testDayjs = dayjs
testDayjs.extend(utc)
testDayjs.extend(timezone)
testDayjs.tz.setDefault('UTC')

Такой подход предотвращает влияние глобальных изменений на другие тесты.


Проверка бизнес-логики с временными зонами

В реальных приложениях часовые пояса часто используются для:

  • расписаний
  • дедлайнов
  • логирования событий
  • планировщиков задач

Пример теста бизнес-логики:

function isExpired(dateString, zone) {
  return dayjs.tz(dateString, zone).isBefore(dayjs())
}

jest.setSystemTime(new Date('2025-01-01T12:00:00Z'))

expect(isExpired('2024-12-31 23:59', 'UTC')).toBe(true)

Стабильность тестов в CI/CD

В CI-средах часто встречаются отличия:

  • Alpine Linux может иметь урезанные timezone-данные
  • Node.js может использовать другой набор IANA
  • отсутствует локальная временная зона пользователя

Поэтому стандартной практикой становится:

TZ=UTC NODE_ENV=test jest --runInBand

и обязательное использование UTC в тестовой среде.