Мокирование локалей

В тестировании кода, использующего Intl API, ключевая проблема заключается в том, что результаты форматирования зависят от локали окружения. Одна и та же функция может возвращать разные строки на машине разработчика, в CI и в браузере пользователя. Это делает тесты нестабильными и трудно воспроизводимыми.

API Intl опирается на ICU (International Components for Unicode), а значит результат формируется с учётом:

  • системной локали (en-US, ru-RU, de-DE)
  • настроек среды выполнения (Node.js, браузер)
  • доступных ICU-данных (full ICU vs small ICU в Node.js)
  • региональных правил форматирования дат, чисел и валют

Даже такая простая операция:

new Intl.DateTimeFormat().format(new Date("2024-01-01"))

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

Особенно критично это проявляется в:

  • форматировании дат и времени
  • округлении и разделителях чисел
  • сравнении строк через Intl.Collator
  • выборе языковых правил (plural rules)

Проблема нестабильных тестов

Тесты, завязанные на Intl, часто ломаются при смене:

  • операционной системы
  • языка системы
  • версии Node.js
  • конфигурации CI

Пример хрупкого теста:

test("format date", () => {
  const formatter = new Intl.DateTimeFormat();
  expect(formatter.format(new Date("2024-01-01"))).toBe("1/1/2024");
});

Такой тест зависит от локали en-US. В ru-RU результат будет другим, и тест упадёт без изменения логики приложения.

Подходы к контролю локали

Явная передача locale в Intl

Самый базовый способ устранить неопределённость — всегда задавать локаль явно:

new Intl.DateTimeFormat("en-US").format(date);
new Intl.NumberFormat("de-DE").format(1234.5);

Это снижает вариативность, но не решает проблему тестирования логики, завязанной на системную локаль.

Мокирование через фиксацию Intl в тестовой среде

Подмена глобальной локали через Intl API

В некоторых случаях можно переопределить конструкторы Intl-объектов:

const originalDateTimeFormat = Intl.DateTimeFormat;

beforeAll(() => {
  Intl.DateTimeFormat = function (locale, options) {
    return new originalDateTimeFormat("en-US", options);
  };
});

afterAll(() => {
  Intl.DateTimeFormat = originalDateTimeFormat;
});

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

Однако он имеет ограничения:

  • не покрывает все методы Intl
  • может конфликтовать с другими тестами
  • не учитывает сложные цепочки вызовов (NumberFormat, Collator)

Мокирование через dependency injection

Более устойчивый подход — не использовать Intl напрямую в бизнес-логике.

function createDateFormatter(locale) {
  return new Intl.DateTimeFormat(locale);
}

function formatDate(date, formatter) {
  return formatter.format(date);
}

Тест:

test("format date", () => {
  const formatter = {
    format: () => "01.01.2024"
  };

  expect(formatDate(new Date(), formatter)).toBe("01.01.2024");
});

Здесь Intl полностью исключён из теста, что делает его детерминированным.

Фиксация окружения Node.js

В Node.js поведение Intl зависит от переменных окружения:

  • LANG
  • LC_ALL
  • TZ

Пример фиксации:

LC_ALL=en_US.UTF-8 TZ=UTC node test.js

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

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

Но такой подход плохо масштабируется в CI, так как требует синхронизации конфигураций между системами.

Использование глобального мокинга через Jest

В тестовых раннерах можно централизованно стабилизировать Intl.

beforeAll(() => {
  const original = Intl.NumberFormat;

  Intl.NumberFormat = function (locale, options) {
    return new original("en-US", options);
  };
});

Для комплексного контроля часто комбинируют несколько конструкторов:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.RelativeTimeFormat
  • Intl.PluralRules

Недостаток подхода — необходимость поддерживать синхронность всех моков при изменениях API.

Использование библиотек для фиксации локали

В экосистеме существуют утилиты, позволяющие стабилизировать Intl:

  • полифиллы ICU
  • тестовые фикстуры локалей
  • изоляция Intl через sandbox

Они обычно:

  • подменяют ICU данные
  • фиксируют locale fallback chain
  • обеспечивают предсказуемый формат вывода

Проблема скрытых зависимостей в Intl

Intl часто используется внутри сторонних библиотек:

  • форматирование дат в UI-фреймворках
  • локализация дат в date-utils
  • сортировка списков

Даже если приложение не использует Intl напрямую, он может влиять на результат через зависимости.

Это усложняет мокирование, так как требуется либо:

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

Мокирование через Proxy

Гибкий способ контроля — использование Proxy для перехвата вызовов:

Intl.DateTimeFormat = new Proxy(Intl.DateTimeFormat, {
  construct(target, args) {
    return new target("en-US", args[1]);
  }
});

Преимущество — минимальное вмешательство в глобальный API.

Недостаток — сложность отладки и потенциальные расхождения с оригинальным поведением.

Стабилизация сравнения строк через Collator

Intl.Collator особенно чувствителен к локали:

const collator = new Intl.Collator("tr");
collator.compare("i", "I");

В разных языках результат может отличаться, что критично для:

  • сортировки списков
  • поиска
  • фильтрации

Мокирование:

Intl.Collator = function () {
  return {
    compare: (a, b) => a.localeCompare(b, "en-US")
  };
};

Изоляция через обёртки над Intl

Практически устойчивый подход — создание слоя абстракции:

export const i18n = {
  formatDate: (date, locale = "en-US") =>
    new Intl.DateTimeFormat(locale).format(date),

  formatNumber: (num, locale = "en-US") =>
    new Intl.NumberFormat(locale).format(num)
};

В тестах:

jest.mock("./i18n", () => ({
  i18n: {
    formatDate: () => "fixed-date",
    formatNumber: () => "1000"
  }
}));

Это полностью разрывает зависимость от Intl.

ICU и различия сборок Node.js

Node.js может быть собран с:

  • full ICU
  • small ICU
  • no ICU (fallback)

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

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

Мокирование в тестах часто скрывает эти различия, но в продакшене они могут проявиться, если тестовое окружение не соответствует production.

Стратегия устойчивого тестирования Intl

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

  • фиксация локали в runtime
  • изоляция Intl через wrapper API
  • мокирование на уровне тестового раннера
  • контроль ICU сборки в CI
  • минимизация прямого использования Intl в бизнес-логике

Такая комбинация снижает вероятность появления нестабильных тестов, связанных с локализацией и региональными форматами.