Unit-тесты локализации

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

Unit-тесты в этом контексте должны обеспечивать не только проверку корректности форматирования, но и устойчивость к вариативности среды выполнения.


Основные компоненты Intl API, используемые в локализации

Intl.DateTimeFormat

Форматирование даты и времени с учётом локали и временной зоны.

const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: '2-digit'
});

formatter.format(new Date('2025-01-15T10:00:00Z'));

Ключевая проблема тестирования — различия в часовом поясе CI-серверов и локальных машин.


Intl.NumberFormat

Форматирование чисел, валют и процентов.

const nf = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
});

nf.format(1234.56);

Результат зависит от правил локали: разделители, порядок символов, пробелы.


Intl.Collator

Сравнение строк с учётом языка.

const collator = new Intl.Collator('sv-SE');

['z', 'a', 'ä'].sort(collator.compare);

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


Проблема нестабильности unit-тестов

Основная сложность тестирования Intl заключается в недетерминированности результата:

  • различия ICU-версий в Node.js
  • различия браузеров
  • влияние системного часового пояса
  • различия локалей по умолчанию
  • флаги окружения (CI vs локальная машина)

Прямое сравнение строк часто приводит к хрупким тестам.


Подходы к тестированию Intl-кода

Фиксация локали как обязательного параметра

Любая функция, использующая Intl, должна принимать локаль явно.

function formatPrice(value, locale) {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: 'USD'
  }).format(value);
}

Тестирование становится детерминированным:

test('format price in en-US', () => {
  expect(formatPrice(1000, 'en-US')).toBe('$1,000.00');
});

Изоляция времени в тестах

Для DateTimeFormat критично фиксировать время.

const date = new Date('2024-06-01T12:00:00Z');

const formatter = new Intl.DateTimeFormat('en-GB', {
  timeZone: 'UTC',
  dateStyle: 'full'
});

formatter.format(date);

Без явного timeZone тест может стать нестабильным.


Использование фиктивного часового пояса

Стратегия заключается в принудительном указании UTC:

new Intl.DateTimeFormat('ru-RU', {
  timeZone: 'UTC'
});

Это устраняет влияние локальной системы.


Снимочные тесты (snapshot testing)

Snapshot-тесты часто применяются для Intl, но требуют строгого контроля.

test('date snapshot', () => {
  const formatter = new Intl.DateTimeFormat('ru-RU', {
    timeZone: 'UTC',
    dateStyle: 'medium'
  });

  expect(formatter.format(new Date('2024-01-01T00:00:00Z'))).toMatchSnapshot();
});

Риски snapshot-подхода:

  • изменение ICU версии ломает все снимки
  • смена Node.js обновляет форматирование
  • сложно ревизировать смысл изменений

Моки Intl API

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

Полный mock

global.Intl.DateTimeFormat = jest.fn(() => ({
  format: () => '01.01.2024'
}));

Проблема такого подхода — потеря смысла теста: проверяется не логика, а заглушка.


Частичный mock (рекомендуемый подход)

const original = Intl.DateTimeFormat;

beforeAll(() => {
  Intl.DateTimeFormat = function (locale, options) {
    const formatter = new original(locale, {
      ...options,
      timeZone: 'UTC'
    });
    return formatter;
  };
});

Такой подход сохраняет реальную работу ICU, но фиксирует среду.


Тестирование локалей и наборов данных

Проверка поддержки локалей

test('supports ru-RU locale', () => {
  const result = new Intl.NumberFormat('ru-RU').format(1000);
  expect(result).toContain('1');
});

Но подобные проверки слабые: они не гарантируют строгий формат.


Параметризованные тесты локалей

const cases = [
  ['en-US', '$1,000.00'],
  ['de-DE', '1.000,00 $'],
  ['ru-RU', '1 000,00 $']
];

test.each(cases)('currency formatting %s', (locale, expected) => {
  const nf = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: 'USD'
  });

  expect(nf.format(1000)).toBe(expected);
});

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


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

Сортировка зависит от языковых правил, поэтому важно фиксировать locale.

test('sorts German umlauts correctly', () => {
  const collator = new Intl.Collator('de-DE');

  const input = ['a', 'ä', 'b'];
  const result = input.sort(collator.compare);

  expect(result).toEqual(['a', 'ä', 'b']);
});

Без Collator сортировка будет зависеть от Unicode-бинарного порядка и даст некорректный результат.


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

Влияние системного часового пояса

new Intl.DateTimeFormat('en-US').format(new Date());

Результат различается в зависимости от TZ.

Решение — фиксация:

process.env.TZ = 'UTC';

или явное указание:

timeZone: 'UTC'

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

В Jest:

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

Это обеспечивает стабильность всех Date-операций.


Проверка границ форматов

Intl API часто зависит от диапазонов значений.

Числа

test('formats large numbers', () => {
  const nf = new Intl.NumberFormat('en-US');
  expect(nf.format(1000000)).toBe('1,000,000');
});

Проценты

test('formats percentages', () => {
  const nf = new Intl.NumberFormat('en-US', {
    style: 'percent'
  });

  expect(nf.format(0.25)).toBe('25%');
});

Версионная зависимость ICU

Node.js использует ICU-библиотеку, которая определяет:

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

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

Стратегии стабилизации

  • фиксация версии Node.js
  • использование Docker-образов
  • минимизация snapshot-тестов
  • явное указание параметров Intl

Абстрагирование Intl в слой форматтеров

Практика, упрощающая тестирование — изоляция Intl в отдельный модуль.

export function createCurrencyFormatter(locale) {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: 'USD'
  });
}

Тестируется не Intl напрямую, а обёртка:

test('currency formatter', () => {
  const format = createCurrencyFormatter('en-US');
  expect(format.format(10)).toBe('$10.00');
});

Контроль детерминизма в CI

В CI окружении важно:

  • фиксировать TZ=UTC
  • фиксировать версию Node.js
  • избегать системных локалей
  • явно задавать locale в каждом Intl вызове
  • избегать неявных fallback-локалей

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

  • отсутствие явного locale
  • сравнение результатов без учёта пробелов non-breaking space
  • игнорирование timeZone
  • snapshot без фиксации ICU версии
  • тестирование поведения без проверки спецификации
  • использование системной локали вместо заданной

Практика построения устойчивых тестов

Устойчивые unit-тесты локализации опираются на три принципа:

  • явное управление локалью и временем
  • минимизация зависимости от окружения
  • тестирование контрактов, а не визуального представления

Структура теста обычно включает:

  • фиксированный input (дата, число, строка)
  • фиксированный locale
  • фиксированную конфигурацию Intl
  • строгую проверку результата