Тестирование в разных окружениях

Поведение интернационализации в JavaScript через Intl API зависит от множества факторов, которые выходят за пределы самой спецификации. В тестировании это приводит к нестабильным результатам, которые меняются между окружениями, версиями движков и установленными ICU-данными.


Основная сложность тестирования заключается в том, что форматирование дат, чисел и строк определяется не только кодом, но и внешними данными:

  • версия ICU (International Components for Unicode)
  • операционная система
  • браузер или рантайм
  • локальные настройки окружения
  • полнота локализационных данных

Даже одинаковый код в Node.js и в браузере может давать различный результат.


ICU как фундамент различий

ICU определяет:

  • правила сортировки (collation)
  • форматы дат и времени
  • числовые форматы
  • поддержку локалей

Разные сборки ICU:

  • full ICU — полный набор локалей
  • small ICU — ограниченный набор (часто в embedded сборках)
  • system ICU — зависит от ОС

Результат: одна и та же локаль может отсутствовать или иметь упрощённые правила.


Различия между Node.js и браузерами

Node.js

В Node.js возможны три режима ICU:

  • full-icu (полная поддержка)
  • small-icu (ограниченная локализация)
  • no-icu (fallback на базовые правила)

Это напрямую влияет на:

  • сортировку строк
  • формат дат
  • поддерживаемые локали

Браузеры

Разные браузеры используют разные реализации ICU:

  • Google Chrome — ICU внутри V8 + собственные патчи
  • Firefox — интеграция ICU через SpiderMonkey
  • Safari — ICU + системные зависимости Apple

Различия проявляются в:

  • порядке сортировки
  • округлении чисел
  • отображении дат
  • поддержке локалей

Влияние движка V8

V8 определяет:

  • реализацию Intl
  • интеграцию ICU
  • оптимизации форматирования

Даже в пределах одного браузера поведение может меняться при обновлении движка.


Проблемы тестирования дат и времени

Зависимость от системного времени

new Intl.DateTimeFormat('ru-RU').format(new Date())

Результат зависит от:

  • системной даты
  • часового пояса
  • переходов на летнее время

Часовые пояса

Разные окружения:

  • UTC в CI
  • локальный timezone разработчика
  • контейнер Docker без timezone

Это приводит к различным форматам времени.


Проблема локалей

new Intl.NumberFormat('de-DE').format(1234567.89)

Ожидаемый результат:

  • 1.234.567,89

Но может отличаться, если:

  • локаль отсутствует в ICU
  • используется fallback locale
  • система ограничена small-icu

Сортировка строк (Collation)

['ä', 'a', 'z'].sort(new Intl.Collator('de').compare)

Различия:

  • разные правила сортировки
  • разные версии Unicode
  • различия в accent sensitivity

Особенно нестабильны тесты при:

  • snapshot testing
  • сравнении массивов строк

Нестабильность snapshot-тестов

Snapshot-тесты с Intl часто ломаются из-за:

  • обновлений ICU
  • изменения браузера в CI
  • смены timezone

Типичный пример проблемы:

expect(formatDate(date)).toMatchSnapshot();

Результат может меняться без изменения кода.


Детеминизм в тестах

Для стабилизации тестов используются:

Фиксация timezone

TZ=UTC

или

process.env.TZ = 'UTC';

Фиксация даты

jest.useFakeTimers();
jest.setSystemTime(new Date('2020-01-01T00:00:00Z'));

Явное указание локали

new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' })

Использование formatToParts

Метод formatToParts снижает хрупкость тестов:

new Intl.DateTimeFormat('ru-RU').formatToParts(new Date());

Вместо строки:

01.01.2020

возвращается структура:

[
  { type: 'day', value: '01' },
  { type: 'literal', value: '.' },
  { type: 'month', value: '01' },
  { type: 'literal', value: '.' },
  { type: 'year', value: '2020' }
]

Это позволяет:

  • тестировать структуру, а не формат
  • избегать локализационных различий

Различия ICU между окружениями CI

CI-системы часто используют:

  • минимальные Docker-образы
  • Alpine Linux (musl)
  • урезанные ICU пакеты

Последствия:

  • отсутствие некоторых локалей
  • fallback на en-US
  • различия в сортировке

Docker и воспроизводимость

Даже в Docker:

  • ICU может быть system-dependent
  • timezone может быть UTC или local
  • locale environment variables могут отсутствовать

Для стабилизации:

  • фиксируется LANG
  • фиксируется LC_ALL
  • устанавливается full-icu пакет

Политика fallback локалей

Intl использует цепочку fallback:

de-CH → de → en → system default

Это влияет на:

  • формат дат
  • числовые разделители
  • названия месяцев

Тесты должны учитывать, что результат может быть “упрощённым”.


Сравнение строк и Unicode нормализация

Разные окружения могут отличаться:

  • NFC vs NFD нормализация
  • порядок диакритических знаков
'a\u030a' !== 'å'

Intl может возвращать разные формы, что ломает строгие сравнения.


Практики стабилизации тестов

1. Изоляция Intl в отдельный модуль

Позволяет подменять реализацию:

export const formatDate = (date) =>
  new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' }).format(date);

2. Dependency injection для locale

function createFormatter(locale) {
  return new Intl.NumberFormat(locale);
}

3. Моки Intl

В тестовой среде иногда подменяется:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.Collator

4. Снимки только с UTC

UTC устраняет:

  • DST эффекты
  • локальные смещения

Особенности новых API Intl

Некоторые расширения Intl усиливают нестабильность:

  • Intl.RelativeTimeFormat
  • Intl.PluralRules
  • Intl.ListFormat
  • Intl.Segmenter

Причина:

  • сложные языковые правила
  • зависимость от ICU версии
  • региональные исключения

Различия между версиями ECMAScript

ECMAScript периодически расширяет Intl API, и это приводит к:

  • появлению новых локалей
  • изменению поведения fallback
  • уточнению правил форматирования

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

  • сравнение строк вместо структуры
  • отсутствие фиксации timezone
  • отсутствие фиксации ICU версии
  • тестирование локалей без fallback стратегии
  • использование snapshot без стабилизации окружения

Контроль версии ICU в Node.js

В Node.js можно контролировать ICU:

  • сборка full-icu
  • использование --icu-data-dir
  • установка full-icu пакета

Это критично для CI, где поведение может отличаться от локальной машины.


Итоговая модель стабильного тестирования Intl

Стабильное поведение достигается комбинацией:

  • фиксированного timezone (UTC)
  • фиксированной локали
  • одинаковой ICU сборки
  • использования formatToParts вместо строк
  • отказа от snapshot для локализаций без стабилизации
  • контроля runtime (браузер/Node версии)

Такое окружение позволяет минимизировать расхождения между системами и делает поведение Intl API предсказуемым при автоматизированном тестировании.