Тестирование снимками
Снимочные тесты — очень полезный инструмент, когда нужно убедиться, что пользовательский интерфейс не изменяется неожиданным образом.
Типичный снимочный тест отображает компонент пользовательского интерфейса, делает снимок, а затем сравнивает его с эталонным файлом снимка, хранящимся рядом с тестом. Тест завершится с ошибкой, если снимки не совпадут: либо изменение неожиданное, либо эталонный снимок нужно обновить до новой версии компонента пользовательского интерфейса.
Тестирование снимками с Jest
Аналогичный подход можно применять при тестировании компонентов React. Вместо отображения графического пользовательского интерфейса, для которого пришлось бы собирать всё приложение, можно использовать тестовый рендерер, чтобы быстро получить сериализуемое значение для компонента React. Рассмотрим пример теста для компонента Link:
import {render} from '@testing-library/react';
import Link from '../Link';
it('renders correctly', () => {
const {container} = render(
<Link page="http://www.facebook.com">Facebook</Link>,
);
expect(container.firstChild).toMatchSnapshot();
});
При первом запуске этого теста Jest создаёт файл снимка, который выглядит так:
exports[`renders correctly 1`] = ` <a aria-label="normal" class="normal" href="http://www.facebook.com" > Facebook </a> `;
Артефакт снимка следует фиксировать вместе с изменениями кода и проверять в рамках проверки кода. Jest использует pretty-format, чтобы снимки было удобно читать при проверке кода. При последующих запусках тестов Jest сравнивает отрисованный результат с предыдущим снимком. Если они совпадают, тест пройдёт успешно. Если нет, то либо средство запуска тестов обнаружило ошибку в вашем коде (в данном случае в компоненте <Link>), которую нужно исправить, либо реализация изменилась и снимок необходимо обновить.
Снимок непосредственно привязан к отображаемым данным — в нашем примере к компоненту <Link> с переданным ему свойством page. Это значит, что даже если в другом файле отсутствуют свойства (например, App.js) компонента <Link>, тест всё равно пройдёт, поскольку тесту неизвестно, как используется компонент <Link>, и он привязан только к Link.js. Кроме того, отображение того же компонента с другими свойствами в других снимочных тестах не повлияет на первый тест, поскольку тесты не знают друг о друге.
Дополнительную информацию о том, как работает тестирование снимками и зачем мы его создали, можно найти в публикации о выпуске. Чтобы лучше понять, когда следует использовать тестирование снимками, рекомендуем прочитать эту публикацию в блоге. Также рекомендуем посмотреть это видео egghead о тестировании снимками с Jest.
Обновление снимков
Если снимочный тест завершается с ошибкой после появления дефекта, это легко заметить. В таком случае исправьте проблему и убедитесь, что снимочные тесты снова проходят. Теперь рассмотрим случай, когда снимочный тест завершается с ошибкой из-за преднамеренного изменения реализации.
Такая ситуация может возникнуть, если мы намеренно изменим адрес, на который указывает компонент Link из нашего примера.
// Updated test case with a Link to a different address
it('renders correctly', () => {
const {container} = render(
<Link page="http://www.instagram.com">Instagram</Link>,
);
expect(container.firstChild).toMatchSnapshot();
});
В этом случае Jest выведет следующий результат:

Поскольку мы только что изменили адрес, на который указывает наш компонент, вполне ожидаемо, что снимок этого компонента изменится. Наш снимочный тест завершается с ошибкой, потому что снимок обновлённого компонента больше не совпадает с артефактом снимка для этого теста.
Чтобы исправить это, нужно обновить артефакты снимков. Можно запустить Jest с флагом, который укажет ему заново сгенерировать снимки:
jest --updateSnapshot
Примите изменения, запустив приведённую выше команду. Если хотите, для повторной генерации снимков можно также использовать эквивалентный флаг из одного символа -u. Это приведёт к повторной генерации артефактов снимков для всех снимочных тестов, завершившихся с ошибкой. Если бы какие-либо другие снимочные тесты завершились с ошибкой из-за непреднамеренного дефекта, перед повторной генерацией снимков пришлось бы исправить его, чтобы не сохранить снимки с ошибочным поведением.
Чтобы ограничить набор снимочных тестов, для которых будут повторно сгенерированы снимки, можно передать дополнительный флаг --testNamePattern. Тогда снимки будут записаны заново только для тестов, соответствующих шаблону.
Попробуйте эту возможность: клонируйте пример со снимками, измените компонент Link и запустите Jest.
Интерактивный режим работы со снимками
Снимки, для которых тесты завершились с ошибкой, также можно обновить в интерактивном режиме наблюдения:
После перехода в интерактивный режим работы со снимками Jest поочерёдно покажет снимки, для которых тесты завершились с ошибкой, и позволит проверить результат.
Затем можно обновить этот снимок или перейти к следующему:

По завершении Jest покажет сводку и вернётся в режим наблюдения:
Встроенные снимки
Встроенные снимки работают так же, как и внешние снимки (файлы .snap), но значения снимков автоматически записываются обратно в исходный код. Это позволяет пользоваться преимуществами автоматически создаваемых снимков, не переключаясь на внешний файл, чтобы убедиться, что записано правильное значение.
Пример:
Сначала напишите тест, вызвав .toMatchInlineSnapshot() без аргументов:
it('renders correctly', () => {
const {container} = render(
<Link page="https://example.com">Example Site</Link>,
);
expect(container.firstChild).toMatchInlineSnapshot();
});
При следующем запуске Jest будет вычислено tree, и снимок будет записан в качестве аргумента для toMatchInlineSnapshot:
it('renders correctly', () => {
const {container} = render(
<Link page="https://example.com">Example Site</Link>,
);
expect(container.firstChild).toMatchInlineSnapshot(`
<a
aria-label="normal"
className="normal"
href="https://example.com"
>
Example Site
</a>
`);
});
Вот и всё! Обновить снимки можно даже с помощью --updateSnapshot или клавиши u в режиме --watch.
По умолчанию запись снимков в исходный код выполняет Jest. Однако если в проекте используется prettier, Jest обнаружит это и поручит эту работу prettier (в том числе с учётом вашей конфигурации).
Сопоставители свойств
Часто в объекте, для которого нужно сделать снимок, встречаются генерируемые поля (например, идентификаторы и даты). Если попытаться сделать снимок таких объектов, тест будет завершаться с ошибкой при каждом запуске:
it('will fail every time', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot();
});
// Snapshot
exports[`will fail every time 1`] = `
{
"createdAt": 2018-05-19T23:36:09.816Z,
"id": 3,
"name": "LeBron James",
}
`;
В таких случаях Jest позволяет указать асимметричный сопоставитель для любого свойства. Эти сопоставители проверяются перед записью или проверкой снимка, а затем сохраняются в файле снимка вместо полученного значения:
it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
// Snapshot
exports[`will check the matchers and pass 1`] = `
{
"createdAt": Any<Date>,
"id": Any<Number>,
"name": "LeBron James",
}
`;
Любое заданное значение, которое не является сопоставителем, будет проверено точно и сохранено в снимке:
it('will check the values and pass', () => {
const user = {
createdAt: new Date(),
name: 'Bond... James Bond',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
name: 'Bond... James Bond',
});
});
// Snapshot
exports[`will check the values and pass 1`] = `
{
"createdAt": Any<Date>,
"name": 'Bond... James Bond',
}
`;
Если речь идёт о строке, а не об объекте, перед проверкой снимка необходимо самостоятельно заменить в этой строке случайную часть.
Для этого можно использовать, например, replace() и регулярные выражения.
const randomNumber = Math.round(Math.random() * 100);
const stringWithRandomData = `<div id="${randomNumber}">Lorem ipsum</div>`;
const stringWithConstantData = stringWithRandomData.replace(/id="\d+"/, 123);
expect(stringWithConstantData).toMatchSnapshot();
Это также можно сделать с помощью сериализатора снимков или создания мока библиотеки, отвечающей за генерацию случайной части кода, для которого создаётся снимок.
Рекомендации
Снимки — отличный инструмент для выявления неожиданных изменений интерфейса приложения, будь то ответ API, пользовательский интерфейс, журналы или сообщения об ошибках. Как и в случае с любой стратегией тестирования, есть рекомендации, о которых нужно знать и которым следует следовать, чтобы эффективно использовать снимки.
1. Относитесь к снимкам как к коду
Фиксируйте снимки и проверяйте их в рамках обычного процесса проверки кода. Это значит, что к снимкам нужно относиться так же, как к любому другому виду тестов или кода в проекте.
Следите за тем, чтобы снимки было легко читать: делайте их целевыми и короткими, а также используйте инструменты, которые помогают соблюдать эти стилистические правила.
Как упоминалось выше, Jest использует pretty-format, чтобы снимки было удобно читать, но может быть полезно добавить и другие инструменты, например eslint-plugin-jest с параметром no-large-snapshots или snapshot-diff с возможностью сравнения снимков компонентов, чтобы поощрять фиксацию коротких и целевых проверок.
Цель — упростить проверку снимков в запросах на слияние и избавиться от привычки заново генерировать снимки при сбое наборов тестов, вместо того чтобы искать первопричины сбоя.
2. Тесты должны быть детерминированными
Тесты должны быть детерминированными. При многократном запуске одних и тех же тестов для неизменившегося компонента каждый раз должны получаться одинаковые результаты. Вы несёте ответственность за то, чтобы создаваемые снимки не содержали зависящих от платформы или других недетерминированных данных.
Например, если у вас есть компонент Clock, использующий Date.now(), снимок, созданный для этого компонента, будет отличаться при каждом запуске теста. В этом случае можно создать мок метода Date.now(), чтобы он возвращал одно и то же значение при каждом запуске теста:
Date.now = jest.fn(() => 1_482_363_367_071);
Теперь при каждом запуске снимочного теста Date.now() будет стабильно возвращать 1482363367071. Благодаря этому для компонента будет создаваться один и тот же снимок независимо от времени запуска теста.
3. Используйте информативные имена снимков
Всегда старайтесь давать тестам и/или снимкам информативные имена. Лучшие имена описывают ожидаемое содержимое снимка. Так проверяющим будет проще оценить снимки, а всем остальным — понять, соответствует ли устаревший снимок ожидаемому поведению, прежде чем обновлять его.
Например, сравните:
exports[`<UserName /> should handle some test case`] = `null`; exports[`<UserName /> should handle some other test case`] = ` <div> Alan Turing </div> `;
С:
exports[`<UserName /> should render null`] = `null`; exports[`<UserName /> should render Alan Turing`] = ` <div> Alan Turing </div> `;
Поскольку второй вариант точно описывает ожидаемый результат, в нём проще заметить ошибку:
exports[`<UserName /> should render null`] = ` <div> Alan Turing </div> `; exports[`<UserName /> should render Alan Turing`] = `null`;
Часто задаваемые вопросы
Записываются ли снимки автоматически в системах непрерывной интеграции (CI)?
Нет. Начиная с Jest 20, снимки в Jest не записываются автоматически при запуске Jest в системе CI, если явно не передан --updateSnapshot. Предполагается, что все снимки входят в код, запускаемый в CI, и поскольку новые снимки автоматически проходят проверку, они не должны проходить тестовый запуск в системе CI. Рекомендуется всегда фиксировать все снимки и хранить их в системе контроля версий.
Нужно ли фиксировать файлы снимков?
Да, все файлы снимков следует фиксировать вместе с модулями, которые они охватывают, и соответствующими тестами. Их следует считать частью теста, наравне со значением любого другого утверждения в Jest. Фактически снимки представляют состояние исходных модулей в конкретный момент времени. Поэтому при изменении исходных модулей Jest может определить, что изменилось по сравнению с предыдущей версией. Кроме того, снимки дают много полезного контекста при проверке кода и помогают проверяющим лучше изучить изменения.
Работает ли тестирование снимками только с компонентами React?
Компоненты React и React Native хорошо подходят для тестирования снимками. Однако снимки могут фиксировать любое сериализуемое значение, поэтому их следует использовать всякий раз, когда нужно проверить правильность результата. В репозитории Jest есть множество примеров проверки результатов работы самого Jest, результатов работы библиотеки утверждений Jest, а также сообщений журнала из разных частей кодовой базы Jest. См. пример создания снимка вывода CLI в репозитории Jest.
В чём разница между тестированием снимками и визуальным регрессионным тестированием?
Тестирование снимками и визуальное регрессионное тестирование — это два разных способа тестирования пользовательских интерфейсов, служащие разным целям. Инструменты визуального регрессионного тестирования делают снимки веб-страниц и сравнивают полученные изображения попиксельно. При тестировании снимками значения сериализуются, сохраняются в текстовых файлах и сравниваются с помощью алгоритма различий. У этих подходов разные компромиссы; причины создания тестирования снимками перечислены в блоге Jest.
Заменяет ли тестирование снимками модульное тестирование?
Тестирование снимками — лишь один из более чем 20 типов утверждений, входящих в Jest. Цель тестирования снимками — не заменить существующие модульные тесты, а дополнить их и упростить тестирование. В некоторых случаях тестирование снимками потенциально может устранить необходимость в модульных тестах для определённого набора функций (например, компонентов React), но оба подхода также могут использоваться вместе.
Каковы скорость тестирования снимками и размер создаваемых файлов?
Jest переписан с учётом производительности, и тестирование снимками не является исключением. Поскольку снимки хранятся в текстовых файлах, этот способ тестирования быстрый и надёжный. Jest создаёт новый файл для каждого файла теста, в котором вызывается сопоставитель toMatchSnapshot. Размер снимков довольно мал: для сравнения, размер всех файлов снимков в самой кодовой базе Jest составляет менее 300 КБ.
Как разрешать конфликты в файлах снимков?
Файлы снимков всегда должны соответствовать текущему состоянию охватываемых ими модулей. Поэтому, если вы объединяете две ветки и сталкиваетесь с конфликтом в файлах снимков, его можно разрешить вручную или обновить файл снимка, запустив Jest и проверив результат.
Можно ли применять принципы разработки через тестирование при тестировании снимками?
Хотя файлы снимков можно создавать вручную, обычно это неудобно. Снимки помогают определить, изменился ли результат работы охватываемых тестами модулей, но не дают рекомендаций по первоначальному проектированию кода.
Работает ли анализ покрытия кода при тестировании снимками?
Да, как и с любыми другими тестами.
© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/snapshot-testing