Snapshot тестирование

Snapshot тестирование в системах интернационализации основано на идее фиксации стабильного состояния переведённого UI или строки и последующего сравнения этого состояния при изменениях. В контексте i18next это означает проверку того, что ключи переводов, интерполяции, плюрализация и форматирование языка не изменяются неожиданно при рефакторинге кода или обновлении словарей.

Snapshot-тестирование фиксирует результат выполнения функции в виде сериализованной структуры (обычно строка или дерево компонентов). При следующем запуске тестов результат сравнивается с сохранённым эталоном.

В случае i18next тестируемыми объектами чаще всего становятся:

  • результат вызова t(key)
  • React-компоненты с useTranslation или <Trans />
  • сформированные строки с интерполяцией
  • структуры, зависящие от текущего языка

Ключевая особенность интернационализации заключается в том, что один и тот же код может давать разные выходные данные в зависимости от активного языка, namespace и контекста.

Базовая настройка i18next для тестов

Перед snapshot-тестированием важно обеспечить детерминированное поведение i18next. Любая нестабильность приведёт к постоянным падениям тестов.

Типичная конфигурация:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    lng: 'en',
    fallbackLng: 'en',
    debug: false,
    resources: {
      en: {
        translation: {
          hello: 'Hello',
          items: 'Item',
          items_plural: 'Items'
        }
      }
    },
    interpolation: {
      escapeValue: false
    }
  });

export default i18n;

Для тестовой среды важно:

  • фиксировать язык через lng
  • отключать асинхронную загрузку ресурсов
  • избегать внешних API загрузки переводов
  • минимизировать side effects

Snapshot тестирование простых переводов

Наиболее базовый сценарий — проверка функции t.

Пример с Jest:

import i18n from './i18n';

describe('i18next snapshots', () => {
  it('translates simple key', () => {
    const result = i18n.t('hello');
    expect(result).toMatchSnapshot();
  });
});

Снапшот будет содержать:

Hello

При изменении перевода тест начнёт сигнализировать о расхождении.

Проблема нестабильных переводов

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

  • динамических значений (даты, числа)
  • случайных вставок
  • пользовательских данных
  • контекста языка, меняющего форму слов

Пример нестабильного случая:

i18n.t('welcome', { name: 'User' });

Если перевод:

{
  "welcome": "Welcome {{name}}"
}

снапшот будет стабильным. Но если формат изменится:

{
  "welcome": "Hi {{name}}, today is {{date}}"
}

тест начнёт падать из-за появления нового поля.

Snapshot с интерполяцией

Интерполяция — одна из ключевых особенностей i18next. Она требует отдельного внимания при тестировании.

it('interpolates variables correctly', () => {
  const result = i18n.t('welcome', { name: 'Alex' });
  expect(result).toMatchSnapshot();
});

Важно учитывать:

  • фиксированные входные данные
  • отсутствие динамических генераторов
  • контроль формата данных

Плюрализация и snapshot-тесты

i18next поддерживает plural rules, которые зависят от языка.

Пример конфигурации:

{
  "item": "Item",
  "item_plural": "Items"
}

Тестирование:

it('handles plural form (singular)', () => {
  const result = i18n.t('item', { count: 1 });
  expect(result).toMatchSnapshot();
});

it('handles plural form (plural)', () => {
  const result = i18n.t('item', { count: 5 });
  expect(result).toMatchSnapshot();
});

Особенности:

  • поведение зависит от lng
  • разные языки имеют разные правила множественного числа
  • snapshot должен учитывать конкретную локаль

Snapshot тестирование React-компонентов с react-i18next

В React-экосистеме snapshot-тестирование чаще применяется к компонентам, использующим i18next через useTranslation.

Пример компонента:

import React from 'react';
import { useTranslation } from 'react-i18next';

export function Greeting() {
  const { t } = useTranslation();

  return <h1>{t('hello')}</h1>;
}

Тест:

import React from 'react';
import renderer from 'react-test-renderer';
import { Greeting } from './Greeting';
import './i18n';

it('renders translated greeting', () => {
  const tree = renderer.create(<Greeting />).toJSON();
  expect(tree).toMatchSnapshot();
});

Снапшот фиксирует уже не строку, а структуру React-дерева:

<h1>
  Hello
</h1>

Snapshot и контекст языка

i18next позволяет переключать язык динамически, что напрямую влияет на результат snapshot-тестов.

it('renders in different language', () => {
  i18n.changeLanguage('en');
  const enResult = i18n.t('hello');

  i18n.changeLanguage('ru');
  const ruResult = i18n.t('hello');

  expect(enResult).toMatchSnapshot();
  expect(ruResult).toMatchSnapshot();
});

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

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

Для стабильных snapshot-тестов создаётся отдельный экземпляр i18next:

import i18next from 'i18next';

export function createTestI18n() {
  const instance = i18next.createInstance();

  instance.init({
    lng: 'en',
    fallbackLng: 'en',
    resources: {
      en: {
        translation: {
          hello: 'Hello'
        }
      }
    },
    interpolation: {
      escapeValue: false
    }
  });

  return instance;
}

Тест:

it('isolated instance snapshot', () => {
  const i18n = createTestI18n();
  const result = i18n.t('hello');

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

Такой подход исключает влияние глобального состояния.

Snapshot и namespaces

i18next активно использует namespaces для разделения переводов.

i18n.init({
  ns: ['common', 'auth'],
  defaultNS: 'common',
  resources: {
    common: {
      translation: {
        save: 'Save'
      }
    },
    auth: {
      translation: {
        login: 'Login'
      }
    }
  }
});

Тестирование:

it('uses namespace correctly', () => {
  const result = i18n.t('login', { ns: 'auth' });
  expect(result).toMatchSnapshot();
});

Особенность snapshot-тестирования в этом случае — необходимость фиксировать не только ключ, но и контекст namespace.

Форматирование чисел и дат

При использовании i18next совместно с форматированием через Intl появляются дополнительные источники нестабильности.

Пример:

i18n.t('price', { value: 1234.56 });

Если форматирование зависит от локали, snapshot может меняться:

  • разделители тысяч
  • десятичные символы
  • валюта

Для стабилизации тестов используют:

  • фиксацию locale
  • отключение форматирования
  • мокирование Intl

Частые проблемы snapshot-тестирования i18next

1. Асинхронная загрузка переводов

Если используется backend загрузчик, snapshot может сниматься до завершения загрузки.

2. Глобальное состояние i18n

Изменение языка в одном тесте влияет на другие.

3. Нестабильные ресурсы

Переводы, подтягиваемые из API, делают snapshot непредсказуемым.

4. Динамические данные

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

Стратегии стабилизации snapshot-тестов

Используются следующие подходы:

  • фиксированные mock-ресурсы переводов
  • отдельный экземпляр i18next на каждый тест
  • отключение backend-лоадеров
  • явная установка языка перед тестом
  • контроль интерполяции через фиксированные данные
  • минимизация snapshot на уровне строк вместо UI-деревьев

Snapshot как инструмент регрессионного контроля переводов

Snapshot-тестирование в i18next чаще всего применяется не как основной механизм проверки логики, а как механизм обнаружения неожиданных изменений в переводах:

  • изменение формулировок
  • потеря ключей
  • некорректная плюрализация
  • случайное изменение namespace
  • деградация UI из-за обрезанных строк

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