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

Snapshot тестирование — метод автоматизированной проверки пользовательского интерфейса, структуры данных или результата рендеринга компонентов путём сравнения текущего состояния объекта с заранее сохранённым эталоном. В экосистеме JavaScript snapshot-подход особенно широко используется вместе с библиотеками тестирования React, Vue, Angular и Node.js-приложений.

В проектах с локализацией и интернационализацией snapshot-тесты позволяют фиксировать форматирование дат, валют, чисел и сообщений, что делает их полезным инструментом при использовании библиотеки Globalize.


Принцип работы snapshot-тестов

Во время первого запуска теста создаётся снимок состояния — snapshot. Он сохраняется в отдельный файл. При последующих запусках результат сравнивается с эталоном.

Если данные изменились:

  • тест помечается как failed;

  • тестовый фреймворк показывает различия;

  • разработчик принимает решение:

    • изменение ожидаемое — snapshot обновляется;
    • изменение ошибочное — код исправляется.

Типичный жизненный цикл:

  1. Рендер компонента.
  2. Получение результата.
  3. Сериализация.
  4. Сохранение снимка.
  5. Сравнение при следующем запуске.

Зачем snapshot-тестирование используется с Globalize

Библиотека Globalize занимается:

  • форматированием дат;
  • локализацией сообщений;
  • форматированием чисел;
  • валют;
  • plural rules;
  • форматированием времени.

Все эти операции чувствительны к:

  • locale;
  • CLDR-данным;
  • версии ICU;
  • timezone;
  • региональным настройкам.

Snapshot-тесты позволяют зафиксировать корректное поведение локализации.

Пример:

const Globalize = require("globalize");

Globalize.locale("ru");

const formatter = Globalize.numberFormatter();

console.log(formatter(12345.67));

Результат:

12 345,67

Snapshot позволяет гарантировать, что:

  • разделители не изменились;
  • локаль не сломалась;
  • обновление CLDR не изменило форматирование случайно.

Установка окружения

Наиболее распространённая связка:

  • Jest
  • Globalize
  • CLDR-data

Установка:

npm install --save-dev jest
npm install globalize cldr-data

Настройка Globalize для тестов

Подключение CLDR

Globalize требует загрузки CLDR-данных.

Пример настройки:

const Globalize = require("globalize");
const cldr = require("cldr-data");

Globalize.load(
  cldr.entireSupplemental(),
  cldr.entireMainFor("en", "ru")
);

Globalize.locale("ru");

Базовое snapshot-тестирование

Пример с Jest

test("number formatting snapshot", () => {
  const formatter = Globalize.numberFormatter();

  expect(formatter(12345.67)).toMatchSnapshot();
});

После первого запуска создаётся файл:

exports[`number formatting snapshot 1`] = `"12 345,67"`;

Snapshot файловая структура

Jest создаёт директорию:

__snapshots__/

Пример:

tests/
  formatting.test.js
  __snapshots__/
    formatting.test.js.snap

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

Проверка локалей

describe("number formatting", () => {
  test("english locale", () => {
    Globalize.locale("en");

    const formatter = Globalize.numberFormatter();

    expect(formatter(1000.5)).toMatchSnapshot();
  });

  test("russian locale", () => {
    Globalize.locale("ru");

    const formatter = Globalize.numberFormatter();

    expect(formatter(1000.5)).toMatchSnapshot();
  });
});

Snapshot:

exports[`number formatting english locale 1`] = `"1,000.5"`;

exports[`number formatting russian locale 1`] = `"1 000,5"`;

Snapshot форматирования валют

Проверка currency formatter

test("currency formatting", () => {
  Globalize.locale("ru");

  const formatter = Globalize.currencyFormatter("USD");

  expect(formatter(1999.99)).toMatchSnapshot();
});

Результат:

"1 999,99 $"

Snapshot дат

Форматирование дат — одна из самых критичных областей интернационализации.

Пример

test("date formatting", () => {
  Globalize.locale("ru");

  const formatter = Globalize.dateFormatter({
    datetime: "medium"
  });

  const result = formatter(new Date("2025-01-10T12:30:00"));

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

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

Дата зависит от:

  • timezone;
  • ICU;
  • системной локали;
  • платформы;
  • Node.js версии.

Из-за этого тесты могут быть нестабильными.


Фиксация timezone

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

TZ=UTC jest

Либо:

process.env.TZ = "UTC";

Snapshot pluralization

Globalize поддерживает plural rules.

Пример:

Globalize.loadMessages({
  ru: {
    cart: {
      item: {
        one: "товар",
        few: "товара",
        many: "товаров"
      }
    }
  }
});

Тест:

test("plural forms", () => {
  Globalize.locale("ru");

  const pluralGenerator =
    Globalize.pluralGenerator();

  expect([
    pluralGenerator(1),
    pluralGenerator(2),
    pluralGenerator(5)
  ]).toMatchSnapshot();
});

Snapshot локализованных сообщений

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

Globalize.loadMessages({
  ru: {
    hello: "Привет, {name}"
  }
});

test("message formatting", () => {
  Globalize.locale("ru");

  const formatter =
    Globalize.messageFormatter("hello");

  expect(
    formatter({ name: "Иван" })
  ).toMatchSnapshot();
});

Inline snapshots

Jest поддерживает inline snapshots.

Пример

test("inline snapshot", () => {
  Globalize.locale("ru");

  const formatter = Globalize.numberFormatter();

  expect(formatter(5000)).toMatchInlineSnapshot(
    `"5 000"`
  );
});

Преимущества:

  • snapshot рядом с тестом;
  • легче читать;
  • проще ревью.

Недостатки:

  • большие snapshots ухудшают читаемость.

Обновление snapshot

Команда:

jest -u

или:

npm test -- -u

Jest перезапишет snapshots.


Когда snapshot обновлять нельзя

Опасные ситуации:

  • случайное изменение locale;
  • поломка форматирования;
  • обновление CLDR с несовместимыми изменениями;
  • неправильная миграция ICU.

Snapshot нельзя обновлять автоматически без анализа diff.


Snapshot и CI/CD

Snapshot-тесты активно применяются в CI.

Типичный pipeline:

Lint
↓
Unit tests
↓
Snapshot tests
↓
Build

При изменении локализации CI сразу обнаруживает проблему.


Проверка нескольких локалей

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

const locales = ["en", "ru", "de", "fr"];

describe.each(locales)(
  "locale %s",
  locale => {
    test("number formatting", () => {
      Globalize.locale(locale);

      const formatter =
        Globalize.numberFormatter();

      expect(
        formatter(123456.78)
      ).toMatchSnapshot();
    });
  }
);

Snapshot и React

Globalize часто используется внутри React-приложений.

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

function Price({ value }) {
  const formatter =
    Globalize.currencyFormatter("USD");

  return <span>{formatter(value)}</span>;
}

Тест:

import renderer from "react-test-renderer";

test("Price component", () => {
  const tree = renderer
    .create(<Price value={100} />)
    .toJSON();

  expect(tree).toMatchSnapshot();
});

Snapshot и динамические данные

Нестабильные данные делают snapshot бесполезным.

Проблемные значения:

  • Date.now()
  • Math.random()
  • UUID
  • timestamp
  • generated ids

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

Mocking даты

jest.spyOn(Date, "now")
  .mockReturnValue(1000000);

Mocking random

jest.spyOn(Math, "random")
  .mockReturnValue(0.5);

Snapshot и сериализация

Иногда объект слишком сложный.

Пример:

expect(user).toMatchSnapshot();

Snapshot может содержать:

  • служебные поля;
  • методы;
  • внутренние структуры.

Лучше сериализовать данные вручную.


Подготовка данных

Нормализация

const normalized = {
  name: user.name,
  locale: user.locale
};

expect(normalized).toMatchSnapshot();

Snapshot и HTML

Globalize нередко участвует в генерации HTML-шаблонов.

Пример

const html = `
<div>
  ${formatter(1000)}
</div>
`;

expect(html).toMatchSnapshot();

Snapshot и SSR

Server Side Rendering особенно чувствителен к локализации.

Проблемы:

  • разные timezone на сервере;
  • несовпадение locale;
  • отличия ICU между environments.

Snapshot помогает фиксировать результат SSR.


Snapshot и обновления CLDR

Обновление CLDR способно изменить:

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

После обновления snapshots часто меняются массово.


Анализ diff snapshot

Пример diff:

- "1,000.50"
+ "1 000,50"

Такой diff может означать:

  • смену locale;
  • изменение CLDR;
  • ошибку конфигурации.

Избыточные snapshot

Плохая практика:

expect(hugeObject).toMatchSnapshot();

Проблемы:

  • сложно читать;
  • сложно ревьюить;
  • трудно находить ошибки.

Хорошие snapshot-тесты

Качественный snapshot:

  • маленький;
  • предсказуемый;
  • стабильный;
  • читаемый;
  • отражает бизнес-логику.

Snapshot и unit testing

Snapshot не заменяет unit-тесты.

Плохой пример:

expect(result).toMatchSnapshot();

Без явных проверок невозможно понять причину ошибки.


Комбинирование snapshot и assertions

Лучший подход:

expect(result).toBe("1 000,50");

expect(result).toMatchSnapshot();

Snapshot custom serializers

Jest поддерживает сериализаторы.

Пример

expect.addSnapshotSerializer({
  test: value => typeof value === "string",

  print: value => value.trim()
});

Это полезно для:

  • очистки whitespace;
  • удаления динамических частей;
  • стабилизации snapshot.

Snapshot для plural messages

Globalize.loadMessages({
  ru: {
    items: {
      one: "{count} товар",
      few: "{count} товара",
      many: "{count} товаров"
    }
  }
});

Тест:

test("plural messages", () => {
  Globalize.locale("ru");

  const formatter =
    Globalize.messageFormatter("items");

  const values = [
    formatter({ count: 1 }),
    formatter({ count: 2 }),
    formatter({ count: 10 })
  ];

  expect(values).toMatchSnapshot();
});

Snapshot и регрессии локализации

Snapshot отлично обнаруживает:

  • потерянный перевод;
  • неправильную валюту;
  • некорректную plural form;
  • изменение separator;
  • проблемы с Unicode.

Unicode и snapshot

Локализация активно использует Unicode.

Пример:

1 000

Между числами используется non-breaking space.

Визуально:

1 000

может выглядеть одинаково, но snapshot обнаружит различие.


Snapshot и non-breaking spaces

Иногда тесты ломаются из-за:

  • U+00A0
  • U+202F

Для диагностики:

console.log(
  Array.from(value).map(
    ch => ch.charCodeAt(0)
  )
);

Snapshot больших локализационных файлов

При большом количестве locale:

  • snapshots быстро растут;
  • появляются сотни diff;
  • ухудшается производительность.

Стратегии оптимизации

Разделение snapshot

Лучше:

number-formatting.test.js
currency-formatting.test.js
plural.test.js

чем:

globalize-all.test.js

Тестирование edge cases

Важно проверять:

  • отрицательные числа;
  • большие значения;
  • zero plural form;
  • NaN;
  • Infinity;
  • null;
  • undefined.

Пример edge case snapshot

test("negative currency", () => {
  Globalize.locale("en");

  const formatter =
    Globalize.currencyFormatter("USD");

  expect(
    formatter(-500)
  ).toMatchSnapshot();
});

Snapshot и производительность

Слишком большие snapshots:

  • замедляют Jest;
  • ухудшают memory usage;
  • усложняют review.

Когда snapshot использовать не стоит

Snapshot плохо подходит для:

  • сложной бизнес-логики;
  • математических вычислений;
  • алгоритмов;
  • нестабильных данных;
  • больших JSON.

Когда snapshot особенно полезен

Наиболее эффективные сценарии:

  • UI-компоненты;
  • локализация;
  • HTML-рендеринг;
  • форматирование;
  • шаблоны;
  • SSR;
  • i18n;
  • ICU output.

Практический пример комплексного тестирования

describe("Globalize localization", () => {
  beforeAll(() => {
    Globalize.loadMessages({
      ru: {
        hello: "Привет"
      }
    });
  });

  test("currency", () => {
    Globalize.locale("ru");

    const formatter =
      Globalize.currencyFormatter("RUB");

    expect(
      formatter(1500)
    ).toMatchSnapshot();
  });

  test("date", () => {
    Globalize.locale("ru");

    const formatter =
      Globalize.dateFormatter({
        datetime: "short"
      });

    expect(
      formatter(
        new Date("2025-01-01")
      )
    ).toMatchSnapshot();
  });

  test("message", () => {
    Globalize.locale("ru");

    const formatter =
      Globalize.messageFormatter("hello");

    expect(
      formatter()
    ).toMatchSnapshot();
  });
});

Лучшие практики

Минимизировать snapshot

Только необходимые данные:

expect({
  amount,
  locale
}).toMatchSnapshot();

Исключать случайные значения

delete result.timestamp;

Использовать фиксированные даты

new Date("2025-01-01T00:00:00Z")

Изолировать locale

beforeEach(() => {
  Globalize.locale("ru");
});

Контролировать timezone

TZ=UTC

Проверять diff вручную

Автоматическое обновление snapshots без анализа — источник скрытых ошибок.


Типичные ошибки

Огромные snapshots

expect(apiResponse).toMatchSnapshot();

Нестабильные данные

expect({
  id: Math.random()
}).toMatchSnapshot();

Смешивание разных сценариев

expect(allLocales).toMatchSnapshot();

Отсутствие явных assertions

expect(result).toMatchSnapshot();

без:

expect(result).toContain("$");

Архитектура snapshot-тестирования

Крупные проекты обычно строят структуру:

tests/
  snapshots/
    formatting/
    messages/
    currencies/
    plural/

Snapshot review в команде

При code review анализируют:

  • действительно ли изменение ожидаемо;
  • изменился ли Unicode;
  • не поменялась ли locale;
  • не сломалась ли pluralization;
  • не обновился ли CLDR случайно.

Snapshot как защита локализации

Для Globalize snapshot-тестирование особенно важно, поскольку библиотека работает с огромным количеством региональных правил. Даже незначительное изменение конфигурации может привести к массовым визуальным ошибкам:

  • неправильным разделителям;
  • неверному отображению валют;
  • ошибочным plural forms;
  • повреждению Unicode-символов;
  • регрессиям при SSR.

Snapshot-тесты позволяют обнаруживать подобные проблемы автоматически ещё до попадания изменений в production.