Snapshot-тестирование форматирования

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

Intl API в JavaScript построен на ICU (International Components for Unicode), и итоговое представление данных формируется на основе локализованных правил. Даже при одинаковом коде результат может отличаться:

  • различия между версиями Node.js и браузерами;
  • обновления ICU, изменяющие правила форматирования;
  • различия системных локалей;
  • автоматическое определение временной зоны окружением;
  • изменения правил календарей и форматов дат в разных регионах.

Пример потенциально нестабильного форматирования:

const formatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "full",
  timeStyle: "long"
});

formatter.format(new Date("2024-01-01T10:00:00Z"));

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

Snapshot-тестирование как инструмент фиксации форматирования

Snapshot-тестирование фиксирует строковый результат функции и сравнивает его с сохранённым эталоном. В контексте Intl API это означает сохранение результата форматирования чисел, дат или строк в локализованном виде.

Пример с Jest:

test("форматирование даты в ru-RU", () => {
  const formatter = new Intl.DateTimeFormat("ru-RU", {
    year: "numeric",
    month: "long",
    day: "numeric"
  });

  const result = formatter.format(new Date("2024-01-01T00:00:00Z"));

  expect(result).toMatchSnapshot();
});

Первый запуск сохраняет строку в snapshot-файл, последующие — сравнивают результат.

Основные зоны применения Intl в snapshot-тестах

Числовое форматирование

Intl.NumberFormat используется для валют, процентов, компактных чисел:

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

formatter.format(123456.78);

Snapshot фиксирует результат вида:

"123.456,78 €"

При этом важно учитывать, что изменение валютных правил или ICU может менять группировку и символы.

Форматирование дат и времени

DateTimeFormat наиболее чувствителен к окружению:

const formatter = new Intl.DateTimeFormat("en-GB", {
  weekday: "long",
  year: "numeric",
  month: "short",
  day: "2-digit"
});

formatter.format(new Date("2025-05-27T12:00:00Z"));

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

  • UTC
  • Europe/London
  • Asia/Almaty

Стабилизация окружения для предсказуемых snapshot

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

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

new Intl.NumberFormat("en-US")
new Intl.DateTimeFormat("en-US")

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

Критический фактор для DateTimeFormat:

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

Без этого snapshot может ломаться при запуске на разных CI-агентах.

Контроль системного времени

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

beforeAll(() => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date("2024-01-01T00:00:00Z"));
});

afterAll(() => {
  jest.useRealTimers();
});

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

Snapshot нормализация результатов Intl

В реальных проектах часто применяется слой нормализации, чтобы минимизировать влияние ICU-версий.

Удаление неразрывных пробелов

Intl часто использует NBSP (\u00A0):

function normalizeIntlString(str) {
  return str.replace(/\u00A0/g, " ");
}

Snapshot:

expect(normalizeIntlString(result)).toMatchSnapshot();

Унификация форматирования чисел

Некоторые среды по-разному трактуют разделители:

function normalizeNumberFormatting(str) {
  return str
    .replace(/\u202F/g, " ")
    .replace(/\u00A0/g, " ");
}

Snapshot для RelativeTimeFormat

RelativeTimeFormat особенно зависит от языковых данных ICU:

const rtf = new Intl.RelativeTimeFormat("ru", { numeric: "auto" });

rtf.format(-1, "day");
rtf.format(1, "month");

Возможные результаты:

  • «вчера»
  • «через 1 месяц»

Snapshot-тестирование здесь чувствительно к изменениям языковых пакетов.

Collator и сортировка строк

Intl.Collator используется для локализованной сортировки:

const collator = new Intl.Collator("de", { sensitivity: "base" });

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

Snapshot фиксирует порядок массива, но результат может меняться при обновлении ICU.

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

Зависимость от окружения CI

Разные CI-агенты могут иметь:

  • разные версии Node.js;
  • разные ICU build flags;
  • разные системные локали.

Это приводит к «ложным падениям» snapshot-тестов.

Часовые пояса и летнее время

DateTimeFormat может изменять:

  • смещение времени;
  • название зоны;
  • формат отображения даты.

Особенно критично при переходах DST.

Эволюция стандартов Unicode

Обновления Unicode могут менять:

  • правила капитализации;
  • разбиение слов;
  • форматирование чисел и валют.

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

Изоляция Intl-слоя

Часто создаётся единый форматтер-слой:

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

Snapshot тестируется уже результат функции, а не напрямую Intl.

Минимизация локалей в тестах

Используются строго фиксированные локали:

  • en-US
  • ru-RU
  • de-DE

Без динамического выбора локали.

Разделение логики и форматирования

Бизнес-логика тестируется отдельно от локализации, чтобы snapshot покрывал только финальный слой представления.

Inline snapshot как инструмент контроля форматирования

Inline snapshots позволяют фиксировать результат прямо в тесте:

expect(formatter.format(1000)).toMatchInlineSnapshot(`"1,000"`);

Это упрощает ревью изменений форматирования, но усиливает зависимость тестов от ICU.

Частичная стабилизация через DI (dependency injection)

Intl можно абстрагировать:

function createFormatters({ locale, timeZone }) {
  return {
    date: new Intl.DateTimeFormat(locale, { timeZone }),
    number: new Intl.NumberFormat(locale)
  };
}

Тесты фиксируют конфигурацию, снижая вариативность окружения.

Типовые ошибки при snapshot Intl

  • отсутствие фиксации timeZone;
  • использование system locale вместо явной;
  • отсутствие моков времени;
  • сравнение строк без нормализации пробелов;
  • смешивание бизнес-логики и форматирования в одном snapshot.

Поведение snapshot при обновлении ICU

При обновлении runtime snapshot может массово измениться без изменения кода. В таких случаях:

  • пересоздаются эталонные снимки;
  • анализируется diff форматирования;
  • проверяется влияние на пользовательский интерфейс.

Это особенно важно для финансовых и дата-интенсивных интерфейсов, где Intl.NumberFormat и Intl.DateTimeFormat влияют на отображение критических данных.