В тестировании кода, использующего Intl API, ключевая
проблема заключается в том, что результаты форматирования зависят от
локали окружения. Одна и та же функция может возвращать разные строки на
машине разработчика, в CI и в браузере пользователя. Это делает тесты
нестабильными и трудно воспроизводимыми.
API Intl опирается на ICU (International Components for
Unicode), а значит результат формируется с учётом:
en-US, ru-RU,
de-DE)Даже такая простая операция:
new Intl.DateTimeFormat().format(new Date("2024-01-01"))
может дать разные результаты в зависимости от окружения.
Особенно критично это проявляется в:
Intl.CollatorТесты, завязанные на Intl, часто ломаются при смене:
Пример хрупкого теста:
test("format date", () => {
const formatter = new Intl.DateTimeFormat();
expect(formatter.format(new Date("2024-01-01"))).toBe("1/1/2024");
});
Такой тест зависит от локали en-US. В ru-RU
результат будет другим, и тест упадёт без изменения логики
приложения.
Самый базовый способ устранить неопределённость — всегда задавать локаль явно:
new Intl.DateTimeFormat("en-US").format(date);
new Intl.NumberFormat("de-DE").format(1234.5);
Это снижает вариативность, но не решает проблему тестирования логики, завязанной на системную локаль.
В некоторых случаях можно переопределить конструкторы
Intl-объектов:
const originalDateTimeFormat = Intl.DateTimeFormat;
beforeAll(() => {
Intl.DateTimeFormat = function (locale, options) {
return new originalDateTimeFormat("en-US", options);
};
});
afterAll(() => {
Intl.DateTimeFormat = originalDateTimeFormat;
});
Этот подход позволяет стабилизировать вывод независимо от окружения.
Однако он имеет ограничения:
IntlNumberFormat,
Collator)Более устойчивый подход — не использовать 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 поведение Intl зависит от переменных
окружения:
LANGLC_ALLTZПример фиксации:
LC_ALL=en_US.UTF-8 TZ=UTC node test.js
Это влияет на:
Но такой подход плохо масштабируется в CI, так как требует синхронизации конфигураций между системами.
В тестовых раннерах можно централизованно стабилизировать
Intl.
beforeAll(() => {
const original = Intl.NumberFormat;
Intl.NumberFormat = function (locale, options) {
return new original("en-US", options);
};
});
Для комплексного контроля часто комбинируют несколько конструкторов:
Intl.DateTimeFormatIntl.NumberFormatIntl.RelativeTimeFormatIntl.PluralRulesНедостаток подхода — необходимость поддерживать синхронность всех моков при изменениях API.
В экосистеме существуют утилиты, позволяющие стабилизировать
Intl:
Intl через sandboxОни обычно:
Intl часто используется внутри сторонних библиотек:
Даже если приложение не использует Intl напрямую, он
может влиять на результат через зависимости.
Это усложняет мокирование, так как требуется либо:
Гибкий способ контроля — использование Proxy для
перехвата вызовов:
Intl.DateTimeFormat = new Proxy(Intl.DateTimeFormat, {
construct(target, args) {
return new target("en-US", args[1]);
}
});
Преимущество — минимальное вмешательство в глобальный API.
Недостаток — сложность отладки и потенциальные расхождения с оригинальным поведением.
Intl.Collator особенно чувствителен к локали:
const collator = new Intl.Collator("tr");
collator.compare("i", "I");
В разных языках результат может отличаться, что критично для:
Мокирование:
Intl.Collator = function () {
return {
compare: (a, b) => a.localeCompare(b, "en-US")
};
};
Практически устойчивый подход — создание слоя абстракции:
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.
Node.js может быть собран с:
Это напрямую влияет на:
Мокирование в тестах часто скрывает эти различия, но в продакшене они могут проявиться, если тестовое окружение не соответствует production.
В реальных системах используется комбинация подходов:
Intl через wrapper APIIntl в
бизнес-логикеТакая комбинация снижает вероятность появления нестабильных тестов, связанных с локализацией и региональными форматами.