Расширения для тестирования

Тестирование кода, использующего Intl API, требует учёта особенностей локализации, зависимости от ICU-данных и различий между средами выполнения. Основная сложность заключается в том, что результат работы большинства форматтеров зависит не только от входных данных, но и от окружения: языка, региональных настроек, версии ICU и даже платформы.

Одной из ключевых проблем является то, что результат форматирования не всегда стабилен между средами. Например, одна и та же дата может быть представлена разными строками в зависимости от локали и версии ICU:

  • различия в порядке компонентов даты;
  • различия в сокращениях месяцев;
  • разные символы пунктуации;
  • отличия в форматах чисел (разделители тысяч и десятичные знаки).

Даже при фиксированной локали поведение может меняться при обновлении рантайма, что делает классическое сравнение строк в тестах ненадёжным.

Стратегии стабилизации поведения Intl

Для тестируемости кода, использующего Intl, применяется несколько подходов:

Фиксация локали

Явное указание локали устраняет часть вариативности:

new Intl.DateTimeFormat("en-US").format(date);

Однако это не решает проблему различий ICU между версиями среды.

Фиксация параметров форматирования

Чем точнее задан формат, тем меньше вариативность результата:

new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2
});

Использование структурного тестирования вместо строк

Вместо сравнения итоговой строки тестируется структура:

  • части даты (год, месяц, день)
  • числовые значения через formatToParts
  • токены форматирования

Пример:

const parts = new Intl.DateTimeFormat("en-US").formatToParts(date);

parts.find(p => p.type === "year").value;

formatToParts как основа тестирования

Метод formatToParts является ключевым инструментом для тестов, поскольку возвращает не строку, а структурированный результат.

Пример разбиения числа:

const parts = new Intl.NumberFormat("de-DE").formatToParts(12345.67);

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

  • integer
  • decimal
  • fraction
  • group

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

Mocking Intl в тестовой среде

Для полного контроля над поведением применяется подмена Intl API.

Подходы:

1. Перезапись глобального Intl

global.Intl.NumberFormat = class {
  constructor() {}
  format(value) {
    return `NUM(${value})`;
  }
};

Используется для изоляции бизнес-логики от локализации.

2. Использование библиотек-стабов

Некоторые тестовые фреймворки позволяют автоматически стабилизировать Intl, фиксируя:

  • локаль
  • таймзону
  • форматирование дат

3. Инъекция форматтера

Вместо прямого использования Intl передаётся зависимость:

function formatPrice(value, formatter) {
  return formatter.format(value);
}

Это позволяет в тестах подставлять детерминированные реализации.

Тестирование дат и временных зон

Наиболее проблемная область — Intl.DateTimeFormat, так как результат зависит от:

  • локали
  • временной зоны
  • летнего времени
  • системных настроек

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

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

В тестовой среде часто используется принудительная установка таймзоны:

process.env.TZ = "UTC";

Это уменьшает вариативность поведения и делает тесты воспроизводимыми.

Тестирование числового форматирования

Intl.NumberFormat относительно стабилен, но имеет особенности:

  • различие в разделителях групп
  • округление
  • представление отрицательных чисел
  • экспоненциальная форма

Для тестирования важно учитывать не строку, а математический результат.

Пример проверки через parts:

const nf = new Intl.NumberFormat("fr-FR");
const parts = nf.formatToParts(-1234.5);

Проверяются:

  • наличие sign
  • корректность integer части
  • fraction округление

Тестирование сортировки (Intl.Collator)

Сортировка строк в разных языках различается существенно.

const collator = new Intl.Collator("de-DE");

В тестах нельзя полагаться на встроенный .sort() без коллатора.

Корректный подход:

array.sort(collator.compare);

Проверка результата должна учитывать:

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

Проблема snapshot-тестирования

Snapshot-тесты плохо сочетаются с Intl API, потому что:

  • локализованные строки часто меняются между версиями ICU
  • разные CI окружения дают разные результаты
  • даже minor update Node.js может изменить формат

Типичная проблема:

Expected: "1,234.50"
Received: "1.234,50"

или изменение кавычек, пробелов, неразрывных символов.

Поэтому snapshot-подход требует фиксации:

  • локали
  • версии ICU
  • таймзоны
  • окружения выполнения

Политика стабильности в CI

Для детерминированных тестов применяются ограничения окружения:

  • фиксированная локаль (en-US или en)
  • UTC как стандарт времени
  • одинаковая версия Node.js
  • отключение системных локалей

Некоторые системы дополнительно используют Docker-образы с предустановленным ICU.

Работа с ICU версиями

Intl API напрямую зависит от ICU-библиотеки.

Разные версии ICU могут менять:

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

Поэтому в больших проектах фиксируется версия ICU через:

  • сборку Node.js
  • контейнеризацию
  • lock версий runtime

Тестирование сегментации текста

Intl.Segmenter используется для разбиения текста на слова, предложения или графемы.

const segmenter = new Intl.Segmenter("en", { granularity: "word" });

В тестах важно учитывать:

  • языковые особенности (китайский, японский)
  • отсутствие пробелов
  • комбинированные символы

Проверка производится не по строкам, а по индексам сегментов.

Изоляция локализационной логики

На практике Intl-логика часто выносится в отдельный слой:

  • форматтеры чисел
  • форматтеры дат
  • коллаторы
  • сегментаторы

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

  • подменять реализацию в тестах
  • централизовать настройки локали
  • уменьшить поверхность нестабильности

Контрактное тестирование Intl-обёрток

Часто проверяется не конкретный результат, а контракт:

  • возвращается строка
  • присутствуют все части даты
  • число не теряет точность
  • сортировка сохраняет порядок

Пример проверки контракта:

  • format() всегда возвращает string
  • formatToParts() всегда возвращает массив объектов
  • compare() всегда возвращает -1, 0 или 1

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