В экосистеме FormatJS компонент IntlProvider выступает
центральной точкой инициализации интернационализации в
React-приложениях. Он предоставляет контекст с локализацией,
сообщениями, форматированием дат, чисел и строк, которые затем
используются всеми дочерними компонентами через хуки и HOC.
В тестовой среде прямое использование реального
IntlProvider часто становится источником нестабильности:
различия локалей, часовых поясов, дефолтных браузерных API и асинхронной
загрузки сообщений приводят к флакованию тестов. Поэтому применяются
моки и стабы, позволяющие фиксировать поведение интернационализации.
Основная цель тестирования компонентов с FormatJS — отделить бизнес-логику от реальных механизмов форматирования. В типичном случае компонент не должен зависеть от:
navigator.language)Intl в Node.js или JSDOMДля этого создаётся фиксированный контекст IntlProvider
с заранее определёнными сообщениями.
Наиболее простой способ стабилизации — создание обёртки с фиксированными параметрами:
import { IntlProvider } from 'react-intl';
const messages = {
greeting: 'Привет',
date_label: 'Дата: {value}',
};
export const MockIntlProvider = ({ children }) => (
<IntlProvider locale="ru-RU" messages={messages}>
{children}
</IntlProvider>
);
Такой стаб устраняет зависимость от внешних источников переводов и фиксирует локаль.
При тестировании компонентов через
@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. В
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 часто становится
источником сложных зависимостей. В тестовой среде она заменяется на
детерминированную функцию:
const intlMock = {
formatMessage: ({ id }, values) => {
if (!values) return id;
return Object.entries(values).reduce(
(acc, [key, val]) => acc.replace(`{${key}}`, val),
id
);
},
};
Это позволяет проверять текстовые шаблоны без реального парсинга ICU-форматов.
Более близкий к реальной реализации способ — использование
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-дерева.
В сложных приложениях используется универсальный фабричный провайдер:
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-правила множественного числа могут вести себя по-разному в разных локалях. Для тестов часто используется упрощённая реализация:
const pluralMessages = {
items: (count) => `items:${count}`,
};
Или замена через мок:
jest.mock('react-intl', () => ({
useIntl: () => ({
formatMessage: ({ id }, values) =>
values?.count ? `${id}:${values.count}` : id,
}),
}));
Компоненты, содержащие собственные провайдеры, требуют особого подхода. В таких случаях применяется двойная стабилизация:
render(
<MockIntlProvider>
<IntlProvider locale="ru-RU" messages={{ inner: 'Внутренний' }}>
<Component />
</IntlProvider>
</MockIntlProvider>
);
Это позволяет контролировать границы контекстов и выявлять утечки локализации.
Snapshot-тестирование с FormatJS требует жёсткой стабилизации форматирования. Без неё снимки становятся нестабильными из-за дат, чисел и локалей.
Фиксация достигается через:
formatDateformatNumberformatMessageexpect(container.firstChild).toMatchSnapshot();
при условии стабильного Intl-контекста.
Нестабильные тесты обычно возникают при следующих условиях:
react-intl без сохранения
оригинальных типовОсобенно критична ситуация, когда useIntl замокан, а
FormattedMessage остаётся реальным компонентом — возникает
рассинхронизация контекста.
В масштабных системах применяется единый тестовый слой:
export const intlTestLayer = {
locale: 'ru-RU',
messages: {
common_ok: 'ОК',
common_cancel: 'Отмена',
},
formatMessage: ({ id }) => id,
formatNumber: (v) => String(v),
formatDate: () => 'fixed-date',
};
Этот объект используется как основа для всех моков, обеспечивая консистентность между различными уровнями тестирования.
В JSDOM отсутствует полноценная реализация Intl API,
поэтому IntlProvider может вести себя иначе, чем в
браузере. Это проявляется в:
Для устранения этих расхождений применяется полная фиксация окружения через тестовые стабы, заменяющие поведение провайдера на предсказуемое.