Добавление тестирования с несколькими снимками в Jest

Приключения в обезьянах

В этом посте я объясню, как обезвредить внутреннюю функциональность Jest, чтобы улучшить процесс разработки, создавая отдельные файлы моментальных снимков для каждого теста. Это особенно полезно для моментальных снимков DOM, которые могут быть довольно большими и трудными для просмотра в контексте управления версиями.

Jest - это популярный фреймворк Facebook для тестирования с открытым исходным кодом в экосистеме javascript. И одна из широко используемых функций этого фреймворка - это тестирование снимков. Из документации:

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

Основы снэпшот-тестирования

Моментальное тестирование - это простая концепция. Вы создаете тест для некоторой перестановки вашего компонента пользовательского интерфейса, написанного на той или иной платформе, файл моментального снимка будет сохранен где-то в вашем проекте для каждого тестового файла. Вы фиксируете моментальные снимки в системе управления версиями, и каждый раз, когда вы (или CI) запускаете тесты, выполняется проверка на случайные изменения пользовательского интерфейса.

Но на самом деле мы можем сделать снимок чего угодно. Для простоты мы будем использовать базовый пример, который пытается сделать снимки чисел 😃:

Несмотря на то, что 100 является константой, мы все равно можем создать ее снимок. При запуске этого произведения искусства с jest новый файл моментального снимка будет создан в каталоге __snapshots__. Имя этого снимка будет именем теста (в нашем примере он будет называться simple_snapshot_1.test.js.snap). Поскольку снимок был создан из числового значения, содержимое файла снимка будет довольно тонким:

exports[`match number in snapshot 1`] = `100`;

Если мы изменим тест сейчас, чтобы он соответствовал другому значению, например 200, и запустим его для существующего снимка, тест завершится неудачно, поскольку полученное значение (200) не совпадает со значением, сохраненным в снимке (100).

Теперь давайте создадим тест, который пытается сделать снимок двух чисел:

Точно так же файл simple_snapshot_2.test.js.snap будет создан в каталоге __snapshots__, и содержимое этого файла будет содержать оба числа:

exports[`match 2 numbers in snapshot 1`] = `150`;

exports[`match 2 numbers in snapshot 2`] = `250`;

Даже если мы создадим тест с несколькими кейсами:

Для этого теста будет один файл моментального снимка -- simple_snapshot_3.test.js.snap, который будет содержать все значения моментального снимка.

По сути, это то, что делает сопоставитель снимков (toMatchSnapshot).

Тестирование с несколькими снимками

Проблема начинается, когда у вас есть большие снимки. Например, у вас есть тест для всех перестановок какой-либо части вашего пользовательского интерфейса в React. В этом случае снимок будет содержать большой кусок разметки. Этот снимок может расти очень быстро, и вы не сможете отслеживать все изменения в нем. Как вы его рассмотрите? Вы хотите зафиксировать в git огромный файл? 😞

Похожая проблема поднята в Storybook здесь. Storybook имеет очень полезный аддон под названием StoryShots. Если вы, как и я, фанат сборников рассказов, вы будете создавать истории почти для всех компонентов своего проекта. Добавляя StoryShots к изображению, вы выиграете автоматизацию моментальных снимков для всех созданных вами историй. Но !! StoryShots работает в рамках одного тестового файла. Это означает, что для всего проекта, указанного в вашей книге рассказов, будет создан очень большой файл моментального снимка. И здесь возникает необходимость разрешить разделение снимков на несколько файлов.

Дело в том, что Facebook на самом деле не хочет поддерживать такое поведение из-за последствий, которые это несет с собой: как отслеживать все эти файлы моментальных снимков, созданные в результате одного теста? Что будет, если вы удалите тест, но из него было создано мало снимков, как они будут очищены? (Вы можете прочитать дискуссии здесь и здесь). И на самом деле я согласен с их аргументами - такое поведение может иметь противоречивые и даже непредсказуемые последствия. Но бывают случаи, когда такая строгая политика нам не нужна. Так что давайте сделаем это сами.

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

Чтобы не конфликтовать с именами Jest, мы будем называть наш сопоставитель - toMatchSpecificSnapshot 😈. Требуемое поведение - предоставить в качестве аргумента путь снимка к сопоставителю. Итак, следующий тест должен выглядеть так:

Поскольку мы создаем настраиваемый сопоставитель, нам нужно зарегистрировать его, чтобы он был известен Jest:

Теперь давайте немного посмотрим, как работает стандартный toMatchSnapshot, чтобы понять, как реализовать наш собственный toMatchSpecificSnapshot сопоставитель. Логика всего снапшота размещена в специальном пакете монорепозитория Jest - здесь. Я не буду подробно останавливаться на внутренней логике пакета jest-snapshot (мне потребовалось некоторое время, чтобы отладить и понять его). Одно из центральных мест - объект State. Он создается внутри компании благодаря глобальной настройке при каждом запуске тестового файла и содержит метаданные снимка.

Позже, когда мы вызываем обычный метод toMatchSnapshot из теста, он проверяет, совпадает ли полученное значение со значением из снимка. Обратите внимание, что переменная snapshotState существует в рамках теста:

Итак, чтобы добиться требуемого поведения, нам необходимо предоставить нашему настраиваемому сопоставителю новый экземпляр состояния, созданный нами, с путем к нашему разделенному снимку:

Теперь нам нужно вызвать функцию toMatchSnapshot, но так, как она будет использовать наше новое состояние внутри 😓… Для этого нам нужно, чтобы метод monkeypatch toMatchSnapshot запускался с контекстом, содержащим новый экземпляр состояния 😰. Звучит запутанно, но давайте посмотрим:

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

Дело в том, что файл моментального снимка на самом деле сохраняется не после вызова toMatchSnapshot, а в конце тестового прогона (потому что, как мы уже говорили, моментальный снимок сохраняется для каждого тестового файла, поэтому Jest необходимо собрать данные из всех тестовых случаев) . Кроме того, Jest вызывает SnapshotState.save() метод экземпляра , созданного настройкой Jest. Но у нас есть еще один экземпляр этого объекта состояния ... Нам нужно вызвать метод save самостоятельно (снова!). Поскольку наша функциональность позволяет хранить один и тот же снимок из разных тестовых файлов и разные снимки из одного и того же тестового файла, нам нужно будет отслеживать все экземпляры состояний снимков и вызывать метод сохранения для всех из них после all Сделаны тесты:

Теперь мы достигли желаемого поведения и можем безопасно запустить тест 🤘:

Заключение

В этом сообщении в блоге я пропустил проблему с настраиваемыми сериализаторами и тот факт, что мы можем запускать Jest с флагом updateSnapshot (jest --updateSnapshot), все это вы можете найти в репозитории здесь.

В конце концов, вы можете согласиться с тем, как это было сделано, или выступить против этого, но я надеюсь, что вы узнали что-то новое 😉

P.S. Этот плагин шутки будет использован в следующем выпуске Storybook (3.3).