Моки и стабы IntlProvider

В экосистеме FormatJS компонент IntlProvider выступает центральной точкой инициализации интернационализации в React-приложениях. Он предоставляет контекст с локализацией, сообщениями, форматированием дат, чисел и строк, которые затем используются всеми дочерними компонентами через хуки и HOC.

В тестовой среде прямое использование реального IntlProvider часто становится источником нестабильности: различия локалей, часовых поясов, дефолтных браузерных API и асинхронной загрузки сообщений приводят к флакованию тестов. Поэтому применяются моки и стабы, позволяющие фиксировать поведение интернационализации.


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

Основная цель тестирования компонентов с FormatJS — отделить бизнес-логику от реальных механизмов форматирования. В типичном случае компонент не должен зависеть от:

  • текущей локали окружения (navigator.language)
  • системного часового пояса
  • реализации Intl в Node.js или JSDOM
  • внешней загрузки переводов

Для этого создаётся фиксированный контекст IntlProvider с заранее определёнными сообщениями.


Базовый стаб IntlProvider

Наиболее простой способ стабилизации — создание обёртки с фиксированными параметрами:

import { IntlProvider } from 'react-intl';

const messages = {
  greeting: 'Привет',
  date_label: 'Дата: {value}',
};

export const MockIntlProvider = ({ children }) => (
  <IntlProvider locale="ru-RU" messages={messages}>
    {children}
  </IntlProvider>
);

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


Использование в React Testing Library

При тестировании компонентов через @testing-library/react IntlProvider интегрируется как обёртка:

import { render } from '@testing-library/react';
import { MockIntlProvider } from './test-utils';
import { MyComponent } from './MyComponent';

const renderWithIntl = (ui) =>
  render(<MockIntlProvider>{ui}</MockIntlProvider>);

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


Моки useIntl

Во многих компонентах используется хук useIntl. В unit-тестах часто требуется заменить его на стабильную реализацию.

import { useIntl } from 'react-intl';

jest.mock('react-intl', () => ({
  ...jest.requireActual('react-intl'),
  useIntl: () => ({
    formatMessage: ({ id }) => id,
    formatNumber: (value) => String(value),
    formatDate: (value) => value.toISOString(),
  }),
}));

Такой мок фиксирует поведение форматирования, превращая его в предсказуемые строки.


Стабилизация formatMessage

Наиболее важная функция formatMessage часто становится источником сложных зависимостей. В тестовой среде она заменяется на детерминированную функцию:

const intlMock = {
  formatMessage: ({ id }, values) => {
    if (!values) return id;
    return Object.entries(values).reduce(
      (acc, [key, val]) => acc.replace(`{${key}}`, val),
      id
    );
  },
};

Это позволяет проверять текстовые шаблоны без реального парсинга ICU-форматов.


Создание тестового Intl объекта через createIntl

Более близкий к реальной реализации способ — использование createIntl из react-intl.

import { createIntl, createIntlCache } from 'react-intl';

const cache = createIntlCache();

export const intl = createIntl(
  {
    locale: 'ru-RU',
    messages: {
      hello: 'Привет',
      items: 'Элементов: {count}',
    },
  },
  cache
);

Данный подход обеспечивает корректную работу ICU-форматирования без поднятия React-дерева.


Подмена IntlProvider через тестовую обёртку

В сложных приложениях используется универсальный фабричный провайдер:

import { IntlProvider } from 'react-intl';

export const buildIntlProvider = (overrides = {}) => {
  const defaultMessages = {
    title: 'Заголовок',
  };

  return ({ children }) => (
    <IntlProvider
      locale={overrides.locale || 'ru-RU'}
      messages={{ ...defaultMessages, ...overrides.messages }}
    >
      {children}
    </IntlProvider>
  );
};

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


Моки временных и числовых форматов

Форматирование дат и чисел часто зависит от среды выполнения. В тестах это приводит к нестабильности, поэтому используется фиксация Intl API:

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

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

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

Это устраняет различия между Node.js и браузерной реализацией.


Стабизация сообщений с ICU plural rules

ICU-правила множественного числа могут вести себя по-разному в разных локалях. Для тестов часто используется упрощённая реализация:

const pluralMessages = {
  items: (count) => `items:${count}`,
};

Или замена через мок:

jest.mock('react-intl', () => ({
  useIntl: () => ({
    formatMessage: ({ id }, values) =>
      values?.count ? `${id}:${values.count}` : id,
  }),
}));

Тестирование компонентов с вложенным IntlProvider

Компоненты, содержащие собственные провайдеры, требуют особого подхода. В таких случаях применяется двойная стабилизация:

render(
  <MockIntlProvider>
    <IntlProvider locale="ru-RU" messages={{ inner: 'Внутренний' }}>
      <Component />
    </IntlProvider>
  </MockIntlProvider>
);

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


Изоляция snapshot-тестов

Snapshot-тестирование с FormatJS требует жёсткой стабилизации форматирования. Без неё снимки становятся нестабильными из-за дат, чисел и локалей.

Фиксация достигается через:

  • мок formatDate
  • мок formatNumber
  • фиксированную локаль
  • детерминированный formatMessage
expect(container.firstChild).toMatchSnapshot();

при условии стабильного Intl-контекста.


Ошибки при неправильном мокировании IntlProvider

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

  • частичная подмена react-intl без сохранения оригинальных типов
  • отсутствие messages в провайдере
  • смешивание реального и мокнутого Intl контекста
  • различие между серверным и браузерным окружением

Особенно критична ситуация, когда useIntl замокан, а FormattedMessage остаётся реальным компонентом — возникает рассинхронизация контекста.


Комбинированные стабы для крупных приложений

В масштабных системах применяется единый тестовый слой:

export const intlTestLayer = {
  locale: 'ru-RU',
  messages: {
    common_ok: 'ОК',
    common_cancel: 'Отмена',
  },
  formatMessage: ({ id }) => id,
  formatNumber: (v) => String(v),
  formatDate: () => 'fixed-date',
};

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


Поведение Provider в изолированных средах

В JSDOM отсутствует полноценная реализация Intl API, поэтому IntlProvider может вести себя иначе, чем в браузере. Это проявляется в:

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

Для устранения этих расхождений применяется полная фиксация окружения через тестовые стабы, заменяющие поведение провайдера на предсказуемое.