Я написал построитель дополнительных отчетов (custom reporter) для Jest и выложил на GitHub. Мой построитель называется Jest-snapshots-book, он создает HTML-книгу снимков компонентов React-приложения.
![](https://habrastorage.org/r/w1560/webt/32/eu/rz/32eurzyx63oqbxjuy5vw8eo3-bu.png)
В статье речь пойдет о том, что такое Jest, snapshot-тестирование, для чего вообще понадобился дополнительный построитель отчетов и как их писать. В основном все это относится к тестированию React-компонентов, но теоретически можно применять при работе с любыми сериализуемыми данными.
Для примера в статье будем тестировать компонент-пагинатор (Paginator). Он является частью нашего проекта-заготовки для создания бессерверных приложений в AWS (GitHub). Задача такого компонента — выводить кнопки для перехода по страницам таблицы или чего-то еще.
Это простой функциональный компонент без собственного состояния (stateless component). В качестве входных данных он получает из props общее количество страниц, текущую страницу и функцию-обработчик нажатия на страницу. На выходе компонент выдает сформированный пагинатор. Для вывода кнопок используется другой дочерний компонент Button. Если страниц много, пагинатор показывает их не все, объединяя их и выводя в виде многоточия.
![](https://habrastorage.org/r/w780q1/webt/9f/5m/ph/9f5mphp5a7hvkj0vmdgfcapz7ve.jpeg)
Jest — это известная opensource-библиотека для модульного тестирования кода JavaScript. Она была создана и развивается благодаря Facebook. Написана на Node.js.
В общих чертах смысл тестирования сводится к тому, что вам нужно придумать входные параметры для вашего кода и тут же описать выходные данные, которые ваш код должен выдать. При выполнении тестов Jest выполняет ваш код с входными параметрами и сверяет результат с ожидаем. Если он совпал, тест пройдет, а если нет — не пройден.
Маленький пример с сайта jestjs.io.
Допустим, у нас есть модуль Node.js, который представляет собой функцию складывающую два числа (файл sum.js):
Если наш модуль сохранен в файле, для его тестирования нам нужно создать файл sum.test.js, в котором написать такой код для тестирования:
В данном примере с помощью функции test мы создали один тест с именем 'adds 1 + 2 to equal 3'. Вторым параметром в функцию test мы передаем функцию, которая собственно и выполняет тест.
Тест состоит в том, что мы выполняем нашу функцию sum с входными параметрами 1 и 2, передаем результат в функцию Jest expect(). Затем с помощью функции Jest toBe() переданный результат сравнивается с ожидаемым (3). Функция toBe() относится к категории проверочных функций Jest (matchers).
Для выполнения тестирования достаточно перейти в папку проекта и вызвать jest в командной строке. Jest найдет файл с расширением .test.js и выполнит тест. Вот такой результат он выведет:
Snapshot-тестирование — это относительная новая возможность в Jest. Смысл заключается в том, что с помощью специальной проверочной функции мы просим Jest сохранить снимок нашей структуры данных на диск, а при последующих выполнениях теста сравнивать новые снимки с ранее сохраненным.
Снимок в данном случае не что иное, как просто текстовое представление данных. Например, вот так будет выглядеть снимок какого-нибудь объекта (ключ массива тут является названием теста):
Вот так выглядит проверочная функция Jest, которая выполняет сравнение снимков (параметры необязательные):
В качестве value может выступать любая сериализуемая структура данных. В первый раз функция toMatchSnapshot() просто запишет снимок на диск, в последующие разы она уже будет выполнять сравнение.
Чаще всего такая технология тестирования используется именно для тестирования React-компонентов, а еще более точно, для тестирования правильности рендеринга React-компонентов. Для этого в качестве value нужно передавать компонент после рендеринга.
Enzyme — это библиотека, которая сильно упрощает тестирование React-приложений, предоставляя удобные функции рендеринга компонентов. Enzyme разработан в Airbnb.
Enzyme позволяет рендерить компоненты в коде. Для этого есть несколько удобных функций, которые выполняют разные варианты рендеринга:
Не будем углубляться во варианты рендеринга, для snapshot-тестирования достаточно статического рендеринга, которое позволяет получить статический HTML-код компонента и его дочерних компонентов:
Итак, мы рендерим наш компонент и передаем результат в expect(), а затем вызываем функцию .toMatchSnapshot(). Функция it — это просто сокращенное имя для функции test.
При каждом выполнении теста toMatchSnapshot() сравнивает два снимка: ожидаемый (который был ранее записан на диск) и актуальный (который получился при текущем выполнении теста).
Если снимки идентичны, тест считается пройденным. Если в снимках есть различие, тест считается не пройденным, и пользователю показывается разница между двумя снимками в виде diff-а (как в системах контроля версий).
Вот пример вывода Jest, когда тест не пройден. Тут мы видим, что у нас в актуальном снимке появилась дополнительная кнопка.
![](https://habrastorage.org/r/w780q1/webt/e2/3j/z1/e23jz1uhhisouenvdsl-64gsz3y.jpeg)
В этой ситуации пользователь должен решить, что делать. Если изменения снимка запланированные ввиду изменения кода компонента, то он должен перезаписать старый снимок новым. А если изменения неожиданные, значит нужно искать проблему в своем коде.
Приведу полный пример для тестирования пагинатора (файл Paginator.test.js).
Для более удобного тестирования пагинатора я создал функцию snapshoot(tp, cp), которая будет принимать двa параметрa: общее количество страниц и текущую страницу. Эта функция будет выполнять тест с заданными параметрами. Дальше остается только вызывать функцию snapshoot() с различными параметрами (можно даже в цикле) и тестировать, тестировать…
Когда я начал работать с этой технологией тестирования, чувство недоделанности изначального подхода не покидало меня. Ведь снимки можно просматривать только в виде текста.
А что если какой-нибудь компонент при рендеринге дает много HTML-кода? Вот компонент-пагинатор, состоящий из 3 кнопок. Снимок такого компонента будет выглядеть так:
Сперва нужно убедиться, что исходная версия компонента правильно рендерится. Не очень-то удобно это делать, просто просматривая HTML-код в текстовом виде. А ведь это всего три кнопки. А если нужно тестировать, например, таблицу или что-то еще более объемное? Причем для полноценного тестирования нужно просматривать множество снимков. Это будет довольно неудобно и тяжело.
Затем, в случае не прохождения теста, вам нужно понять, чем отличается внешний вид компонентов. Diff их HTML-кода, конечно, позволит понять, что изменилось, но опять-таки возможность воочию посмотреть разницу не будет лишней.
В общем я подумал, что надо бы сделать так, чтобы снимки можно было просматривать в браузере в том же виде, как они выглядят в приложении. В том числе с примененными к ним стилями. Так у меня появилась идея улучшить процесс snapshot-тестирования за счет написания дополнительного построителя отчетов для Jest.
Забегая вперед, вот что у меня получилось. Каждый раз при выполнении тестов мой построитель обновляет книгу снимков. Прямо в браузере можно просматривать компоненты так, как они выглядят в приложении, а также смотреть сразу исходный код снимков и diff (если тест не пройден).
![](https://habrastorage.org/webt/vp/pu/kk/vppukk7dddohnzz29gtrkylhsxi.gif)
Создатели Jest предусмотрели возможность написания дополнительных построителей отчетов. Делается это следующим образом. Нужно написать на Node.JS модуль, который должен иметь один или несколько из этих методов: onRunStart, onTestStart, onTestResult, onRunComplete, которые соответствуют различным событиями хода выполнения тестов.
Затем нужно подключить свой модуль в конфиге Jest. Для этого есть специальная директива reporters. Если вы хотите дополнительно включить свой построитель, то нужно добавить его в конец массива reporters.
После этого Jest будет вызывать методы из вашего модуля при наступлении определенных этапов выполнения теста, передавая в методы текущие результаты. Код в данных методах собственно и должен создавать дополнительные отчеты, которые вам нужны. Так в общих чертах выглядит создание дополнительных построителей отчетов.
Код модуля специально не вставляю в статью, так как буду еще его улучшать. Его можно найти на моем GitHub, это файл src/index.js на странице проекта.
Мой построитель отчета вызывается по завершению выполнения тестов. Я поместил код в метод onRunComplete(contexts, results). Он работает следующим образом.
В свойстве results.testResults Jest передает в эту функцию массив результатов тестирования. Каждый результат тестирования включает путь к файлу с тестами и массив сообщений с результатами. Мой построитель отчета ищет для каждого файла с тестами соответствующий файл со снимками. Если файл снимков обнаружен, построитель отчета создает HTML-страницу в книге снимков и записывает ее в папку snapshots-book в корневой папке проекта.
Для формирования HTML-страницы построитель отчета с помощью рекурсивной функции grabCSS(moduleName, css = [], level = 0) собирает все стили, начиная с самого тестируемого компонента и дальше вниз по дереву всех компонентов, которые он импортует. Таким образом, функция собирает все стили, которые нужны для корректного отображения компонента. Собранные стили добавляются в HTML-страницу книги снимков.
В своих проектах я использую CSS-модули, поэтому не уверен, что это будет работать, если CSS-модули не используются.
В случае, если тест пройден, построитель вставляет в HTML-страницу iFrame со снимком в двух вариантах отображения: исходный код (снимок, как он есть) и компонент после рендеринга. Вариант отображения в iFrame меняется по клику мышкой.
Если же тест не был пройден, то все сложнее. Jest предоставляет в этом случае только то сообщение, которое он выводит в консоль (см. скриншот выше).
Оно содержит diff-ы и дополнительные сведения о не пройденном тесте. На самом деле в этом случае мы имеем дело в сущности с двумя снимками: ожидаемым и актуальным. Если ожидаемый у нас есть — он хранится на диске в папке снимков, то актуальный снимок Jest не предоставляет.
Поэтому пришлось написать код, который применяет взятый из сообщения Jest diff к ожидаемому снимку и создает актуальный снимок на основе ожидаемого. После этого построитель выводит рядом с iFrame ожидаемого снимка iFrame актуального снимка, который может менять свое содержимое между тремя вариантами: исходный код, компонент после рендеринга, diff.
Вот так выглядит вывод построителя отчета, если установить для него опцию verbose = true.
![](https://habrastorage.org/r/w780q1/webt/qz/pd/4h/qzpd4hduqakenbx8vjhonggjgso.jpeg)
Snapshot-тестирования не достаточно для полноценного тестирования React-приложения. Оно покрывает только рендеринг ваших компонентов. Нужно еще тестировать их функционирование (реакции на действия пользователей, например). Однако snapshot-тестирование — это очень удобный способ быть уверенным, что ваши компоненты рендерятся так, как было задумано. А jest-snapshots-book делает процесс чуточку легче.
![](https://habrastorage.org/webt/32/eu/rz/32eurzyx63oqbxjuy5vw8eo3-bu.png)
В статье речь пойдет о том, что такое Jest, snapshot-тестирование, для чего вообще понадобился дополнительный построитель отчетов и как их писать. В основном все это относится к тестированию React-компонентов, но теоретически можно применять при работе с любыми сериализуемыми данными.
React-компонент пагинатор
Для примера в статье будем тестировать компонент-пагинатор (Paginator). Он является частью нашего проекта-заготовки для создания бессерверных приложений в AWS (GitHub). Задача такого компонента — выводить кнопки для перехода по страницам таблицы или чего-то еще.
Это простой функциональный компонент без собственного состояния (stateless component). В качестве входных данных он получает из props общее количество страниц, текущую страницу и функцию-обработчик нажатия на страницу. На выходе компонент выдает сформированный пагинатор. Для вывода кнопок используется другой дочерний компонент Button. Если страниц много, пагинатор показывает их не все, объединяя их и выводя в виде многоточия.
![](https://habrastorage.org/webt/9f/5m/ph/9f5mphp5a7hvkj0vmdgfcapz7ve.jpeg)
Код компонента-пагинатора
import React from 'react';
import classes from './Paginator.css';
import Button from '../../UI/Button/Button';
const Paginator = (props) => {
const { tp, cp, pageClickHandler } = props;
let paginator = null;
if (tp !== undefined && tp > 0) {
let buttons = [];
buttons.push(
<Button
key={`pback`}
disabled={cp === 1}
clicked={(cp === 1 ? null : event => pageClickHandler(event, 'back'))}>
←
</Button>
);
const isDots = (i, tp, cp) =>
i > 1 &&
i < tp &&
(i > cp + 1 || i < cp - 1) &&
(cp > 4 || i > 5) &&
(cp < tp - 3 || i < tp - 4);
let flag;
for (let i = 1; i <= tp; i++) {
const dots = isDots(i, tp, cp) && (isDots(i - 1, tp, cp) || isDots(i + 1, tp, cp));
if (flag && dots) {
flag = false;
buttons.push(
<Button
key={`p${i}`}
className={classes.Dots}
disabled={true}>
...
</Button>
);
} else if (!dots) {
flag = true;
buttons.push(
<Button
key={`p${i}`}
disabled={i === cp}
clicked={(i === cp ? null : event => pageClickHandler(event, i))}>
{i}
</Button>
);
}
}
buttons.push(
<Button
key={`pforward`}
disabled={cp === tp}
clicked={(cp === tp ? null : event => pageClickHandler(event, 'forward'))}>
→
</Button>
);
paginator =
<div className={classes.Paginator}>
{buttons}
</div>
}
return paginator;
}
export default Paginator;
Код компонента-кнопки
import React from 'react';
import classes from './Button.css';
const button = (props) => (
<button
disabled={props.disabled}
className={classes.Button + (props.className ? ' ' + props.className : '')}
onClick={props.clicked}>
{props.children}
</button>
);
export default button;
Jest
Jest — это известная opensource-библиотека для модульного тестирования кода JavaScript. Она была создана и развивается благодаря Facebook. Написана на Node.js.
В общих чертах смысл тестирования сводится к тому, что вам нужно придумать входные параметры для вашего кода и тут же описать выходные данные, которые ваш код должен выдать. При выполнении тестов Jest выполняет ваш код с входными параметрами и сверяет результат с ожидаем. Если он совпал, тест пройдет, а если нет — не пройден.
Маленький пример с сайта jestjs.io.
Допустим, у нас есть модуль Node.js, который представляет собой функцию складывающую два числа (файл sum.js):
function sum(a, b) {
return a + b;
}
module.exports = sum;
Если наш модуль сохранен в файле, для его тестирования нам нужно создать файл sum.test.js, в котором написать такой код для тестирования:
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
В данном примере с помощью функции test мы создали один тест с именем 'adds 1 + 2 to equal 3'. Вторым параметром в функцию test мы передаем функцию, которая собственно и выполняет тест.
Тест состоит в том, что мы выполняем нашу функцию sum с входными параметрами 1 и 2, передаем результат в функцию Jest expect(). Затем с помощью функции Jest toBe() переданный результат сравнивается с ожидаемым (3). Функция toBe() относится к категории проверочных функций Jest (matchers).
Для выполнения тестирования достаточно перейти в папку проекта и вызвать jest в командной строке. Jest найдет файл с расширением .test.js и выполнит тест. Вот такой результат он выведет:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
Enzyme и snapshot-тестирование компонентов
Snapshot-тестирование — это относительная новая возможность в Jest. Смысл заключается в том, что с помощью специальной проверочной функции мы просим Jest сохранить снимок нашей структуры данных на диск, а при последующих выполнениях теста сравнивать новые снимки с ранее сохраненным.
Снимок в данном случае не что иное, как просто текстовое представление данных. Например, вот так будет выглядеть снимок какого-нибудь объекта (ключ массива тут является названием теста):
exports[`some test name`] = `
Object {
"Hello": "world"
}
`;
Вот так выглядит проверочная функция Jest, которая выполняет сравнение снимков (параметры необязательные):
expect(value).toMatchSnapshot(propertyMatchers, snapshotName)
В качестве value может выступать любая сериализуемая структура данных. В первый раз функция toMatchSnapshot() просто запишет снимок на диск, в последующие разы она уже будет выполнять сравнение.
Чаще всего такая технология тестирования используется именно для тестирования React-компонентов, а еще более точно, для тестирования правильности рендеринга React-компонентов. Для этого в качестве value нужно передавать компонент после рендеринга.
Enzyme — это библиотека, которая сильно упрощает тестирование React-приложений, предоставляя удобные функции рендеринга компонентов. Enzyme разработан в Airbnb.
Enzyme позволяет рендерить компоненты в коде. Для этого есть несколько удобных функций, которые выполняют разные варианты рендеринга:
- полный рендеринг (как в браузере, full DOM rendering);
- упрощенный рендеринг (shallow rendering);
- статический рендеринг (static rendering).
Не будем углубляться во варианты рендеринга, для snapshot-тестирования достаточно статического рендеринга, которое позволяет получить статический HTML-код компонента и его дочерних компонентов:
const wrapper = render(<Foo title="unique" />);
Итак, мы рендерим наш компонент и передаем результат в expect(), а затем вызываем функцию .toMatchSnapshot(). Функция it — это просто сокращенное имя для функции test.
...
const wrapper = render(<Paginator tp={tp} cp={cp} />);
it(`Total = ${tp}, Current = ${cp}`, () => {
expect(wrapper).toMatchSnapshot();
});
...
При каждом выполнении теста toMatchSnapshot() сравнивает два снимка: ожидаемый (который был ранее записан на диск) и актуальный (который получился при текущем выполнении теста).
Если снимки идентичны, тест считается пройденным. Если в снимках есть различие, тест считается не пройденным, и пользователю показывается разница между двумя снимками в виде diff-а (как в системах контроля версий).
Вот пример вывода Jest, когда тест не пройден. Тут мы видим, что у нас в актуальном снимке появилась дополнительная кнопка.
![](https://habrastorage.org/webt/e2/3j/z1/e23jz1uhhisouenvdsl-64gsz3y.jpeg)
В этой ситуации пользователь должен решить, что делать. Если изменения снимка запланированные ввиду изменения кода компонента, то он должен перезаписать старый снимок новым. А если изменения неожиданные, значит нужно искать проблему в своем коде.
Приведу полный пример для тестирования пагинатора (файл Paginator.test.js).
Для более удобного тестирования пагинатора я создал функцию snapshoot(tp, cp), которая будет принимать двa параметрa: общее количество страниц и текущую страницу. Эта функция будет выполнять тест с заданными параметрами. Дальше остается только вызывать функцию snapshoot() с различными параметрами (можно даже в цикле) и тестировать, тестировать…
import React from 'react';
import { configure, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Paginator from './Paginator';
configure({ adapter: new Adapter() });
describe('Paginator', () => {
const snapshoot = (tp, cp) => {
const wrapper = render(<Paginator tp={tp} cp={cp} />);
it(`Total = ${tp}, Current = ${cp}`, () => {
expect(wrapper).toMatchSnapshot();
});
}
snapshoot(0, 0);
snapshoot(1, -1);
snapshoot(1, 1);
snapshoot(2, 2);
snapshoot(3, 1);
for (let cp = 1; cp <= 10; cp++) {
snapshoot(10, cp);
}
});
Зачем понадобился построитель дополнительных отчетов
Когда я начал работать с этой технологией тестирования, чувство недоделанности изначального подхода не покидало меня. Ведь снимки можно просматривать только в виде текста.
А что если какой-нибудь компонент при рендеринге дает много HTML-кода? Вот компонент-пагинатор, состоящий из 3 кнопок. Снимок такого компонента будет выглядеть так:
exports[`Paginator Total = 1, Current = -1 1`] = `
<div
class="Paginator"
>
<button
class="Button"
>
←
</button>
<button
class="Button"
>
1
</button>
<button
class="Button"
>
→
</button>
</div>
`;
Сперва нужно убедиться, что исходная версия компонента правильно рендерится. Не очень-то удобно это делать, просто просматривая HTML-код в текстовом виде. А ведь это всего три кнопки. А если нужно тестировать, например, таблицу или что-то еще более объемное? Причем для полноценного тестирования нужно просматривать множество снимков. Это будет довольно неудобно и тяжело.
Затем, в случае не прохождения теста, вам нужно понять, чем отличается внешний вид компонентов. Diff их HTML-кода, конечно, позволит понять, что изменилось, но опять-таки возможность воочию посмотреть разницу не будет лишней.
В общем я подумал, что надо бы сделать так, чтобы снимки можно было просматривать в браузере в том же виде, как они выглядят в приложении. В том числе с примененными к ним стилями. Так у меня появилась идея улучшить процесс snapshot-тестирования за счет написания дополнительного построителя отчетов для Jest.
Забегая вперед, вот что у меня получилось. Каждый раз при выполнении тестов мой построитель обновляет книгу снимков. Прямо в браузере можно просматривать компоненты так, как они выглядят в приложении, а также смотреть сразу исходный код снимков и diff (если тест не пройден).
![](https://habrastorage.org/webt/vp/pu/kk/vppukk7dddohnzz29gtrkylhsxi.gif)
Построители дополнительных отчетов Jest
Создатели Jest предусмотрели возможность написания дополнительных построителей отчетов. Делается это следующим образом. Нужно написать на Node.JS модуль, который должен иметь один или несколько из этих методов: onRunStart, onTestStart, onTestResult, onRunComplete, которые соответствуют различным событиями хода выполнения тестов.
Затем нужно подключить свой модуль в конфиге Jest. Для этого есть специальная директива reporters. Если вы хотите дополнительно включить свой построитель, то нужно добавить его в конец массива reporters.
После этого Jest будет вызывать методы из вашего модуля при наступлении определенных этапов выполнения теста, передавая в методы текущие результаты. Код в данных методах собственно и должен создавать дополнительные отчеты, которые вам нужны. Так в общих чертах выглядит создание дополнительных построителей отчетов.
Как устроен Jest-snapshots-book
Код модуля специально не вставляю в статью, так как буду еще его улучшать. Его можно найти на моем GitHub, это файл src/index.js на странице проекта.
Мой построитель отчета вызывается по завершению выполнения тестов. Я поместил код в метод onRunComplete(contexts, results). Он работает следующим образом.
В свойстве results.testResults Jest передает в эту функцию массив результатов тестирования. Каждый результат тестирования включает путь к файлу с тестами и массив сообщений с результатами. Мой построитель отчета ищет для каждого файла с тестами соответствующий файл со снимками. Если файл снимков обнаружен, построитель отчета создает HTML-страницу в книге снимков и записывает ее в папку snapshots-book в корневой папке проекта.
Для формирования HTML-страницы построитель отчета с помощью рекурсивной функции grabCSS(moduleName, css = [], level = 0) собирает все стили, начиная с самого тестируемого компонента и дальше вниз по дереву всех компонентов, которые он импортует. Таким образом, функция собирает все стили, которые нужны для корректного отображения компонента. Собранные стили добавляются в HTML-страницу книги снимков.
В своих проектах я использую CSS-модули, поэтому не уверен, что это будет работать, если CSS-модули не используются.
В случае, если тест пройден, построитель вставляет в HTML-страницу iFrame со снимком в двух вариантах отображения: исходный код (снимок, как он есть) и компонент после рендеринга. Вариант отображения в iFrame меняется по клику мышкой.
Если же тест не был пройден, то все сложнее. Jest предоставляет в этом случае только то сообщение, которое он выводит в консоль (см. скриншот выше).
Оно содержит diff-ы и дополнительные сведения о не пройденном тесте. На самом деле в этом случае мы имеем дело в сущности с двумя снимками: ожидаемым и актуальным. Если ожидаемый у нас есть — он хранится на диске в папке снимков, то актуальный снимок Jest не предоставляет.
Поэтому пришлось написать код, который применяет взятый из сообщения Jest diff к ожидаемому снимку и создает актуальный снимок на основе ожидаемого. После этого построитель выводит рядом с iFrame ожидаемого снимка iFrame актуального снимка, который может менять свое содержимое между тремя вариантами: исходный код, компонент после рендеринга, diff.
Вот так выглядит вывод построителя отчета, если установить для него опцию verbose = true.
![](https://habrastorage.org/webt/qz/pd/4h/qzpd4hduqakenbx8vjhonggjgso.jpeg)
Полезные ссылки
- Jest-snapshots-book
- Исходный код модуля
- Варианты рендеринга Enzyme
- Snapshot-тестирование
- Директива reporters конфига Jest
PS
Snapshot-тестирования не достаточно для полноценного тестирования React-приложения. Оно покрывает только рендеринг ваших компонентов. Нужно еще тестировать их функционирование (реакции на действия пользователей, например). Однако snapshot-тестирование — это очень удобный способ быть уверенным, что ваши компоненты рендерятся так, как было задумано. А jest-snapshots-book делает процесс чуточку легче.