Август 2019: эта статья устарела, проверьте мою новую статью о тестировании компонентов React с помощью Jest и Enzyme.
Октябрь 2017 г .: статья обновлена до React 16 и Enzyme 3.
Некоторые люди говорят, что тестирование компонентов React бесполезно, и во многих случаях это так, но есть несколько случаев, когда я считаю это полезным:
- библиотеки компонентов,
- проекты с открытым исходным кодом,
- интеграция со сторонними компонентами,
- ошибки, чтобы предотвратить регресс.
Я перепробовал множество инструментов и, наконец, нашел комбинацию, которую мне достаточно, чтобы предложить другим разработчикам:
- Шутка, тестовый раннер;
- Enzyme, утилита для тестирования React;
- Энзим-в-json для преобразования энзимных оболочек для сопоставления снимков Jest.
В большинстве своих тестов я использую неглубокий рендеринг со снимками Jest.
Мелкий рендеринг
При поверхностном рендеринге рендерится только сам компонент без его дочерних элементов. Поэтому, если вы измените что-то в дочернем компоненте, это не повлияет на неглубокий вывод вашего компонента. Или ошибка, обнаруженная в дочернем компоненте, не нарушит проверку вашего компонента. Он также не требует DOM.
Например, этот компонент:
const ButtonWithIcon = ({icon, children}) => ( <button><Icon icon={icon} />{children}</button> );
Будет отображаться React следующим образом:
<button> <i class="icon icon_coffee"></i> Hello Jest! </button>
Но вот так с мелким рендерингом:
<button> <Icon icon="coffee" /> Hello Jest! </button>
Обратите внимание, что компонент Icon не был отрисован.
Тестирование снимков
Моментальные снимки Jest похожи на те старые текстовые пользовательские интерфейсы с окнами и кнопками, состоящими из текстовых символов: это визуализированный вывод вашего компонента, хранящийся в текстовом файле.
Вы говорите Jest, что хотите быть уверены, что выходные данные этого компонента никогда не должны изменяться случайно, и Jest сохраняет его в файл, который выглядит следующим образом:
exports[`test should render a label 1`] = ` <label className="isBlock"> Hello Jest! </label> `; exports[`test should render a small label 1`] = ` <label className="isBlock isSmall"> Hello Jest! </label> `;
Каждый раз, когда вы меняете разметку, Jest показывает разницу и просит обновить снимок, если изменение было запланировано.
Помимо ваших тестов, Jest хранит снимки в файлах типа __snapshots __ / Label.spec.js.snap, и вам необходимо зафиксировать их вместе с вашим кодом.
Почему шутка
- Очень быстро.
- Моментальное тестирование.
- Великолепный интерактивный режим просмотра, в котором повторно запускаются только те тесты, которые имеют отношение к вашим изменениям.
- Полезные сообщения об ошибках.
- Простая конфигурация.
- Издевается и шпионы.
- Отчет о покрытии с помощью одного переключателя командной строки.
- Активное развитие.
- Невозможно тихо написать неверные утверждения вроде expect (foo) .to.be.a.function вместо expect (foo) .to.be.a ('function') в Chai, потому что это единственная естественная вещь, которую можно написать после (исправить) ожидать (foo) .to.be.true.
Почему фермент
- Удобные утилиты для работы с мелкой отрисовкой, статической отрисовкой разметки или отрисовкой DOM.
- API, подобный jQuery, для поиска элементов, чтения реквизитов и т. д.
Настройка
Сначала установите все зависимости, включая зависимости одноранговых узлов:
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
Вам также понадобится babel-jest для Babel и ts-jest для TypeScript.
Обновите свой package.json:
"scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "jest": { "setupFiles": ["./test/jestsetup.js"], "snapshotSerializers": ["enzyme-to-json/serializer"] }
snapshotSerializers позволяет передавать оболочки Enzyme непосредственно в средство сопоставления снимков Jest, не преобразовывая их вручную, вызывая функцию toJson для преобразования фермента в json.
Создайте файл test / jestsetup.js для настройки среды Jest (см. SetupFiles выше):
import Enzyme, { shallow, render, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; // React 16 Enzyme adapter Enzyme.configure({ adapter: new Adapter() }); // Make Enzyme functions available in all test files without importing global.shallow = shallow; global.render = render; global.mount = mount;
Для модулей CSS также добавьте в раздел jest вашего package.json:
"jest": { "moduleNameMapper": { "^.+\\.(css|scss)$": "identity-obj-proxy" } }
И запускаем:
npm install --save-dev identity-obj-proxy
Обратите внимание, что identity-obj-proxy требует флага node - harmony-proxies для Node 4 и 5.
Написание тестов
Тестирование рендеринга основных компонентов
Этого достаточно для большинства неинтерактивных компонентов:
test('render a label', () => { const wrapper = shallow( <Label>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); }); test('render a small label', () => { const wrapper = shallow( <Label small>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); }); test('render a grayish label', () => { const wrapper = shallow( <Label light>Hello Jest!</Label> ); expect(wrapper).toMatchSnapshot(); });
Тестирование реквизита
Иногда вы хотите быть более точным и увидеть реальные значения в тестах. В этом случае используйте Enzyme API с обычными утверждениями Jest:
test('render a document title', () => { const wrapper = shallow( <DocumentTitle title="Events" /> ); expect(wrapper.prop('title')).toEqual('Events'); }); test('render a document title and a parent title', () => { const wrapper = shallow( <DocumentTitle title="Events" parent="Event Radar" /> ); expect(wrapper.prop('title')).toEqual('Events — Event Radar'); });
В некоторых случаях снимки просто невозможно использовать. Например, если у вас есть случайные идентификаторы или что-то в этом роде:
test('render a popover with a random ID', () => { const wrapper = shallow( <Popover>Hello Jest!</Popover> ); expect(wrapper.prop('id')).toMatch(/Popover\d+/); });
Тестовые мероприятия
Вы можете смоделировать такое событие, как щелчок или изменение, а затем сравнить компонент со снимком:
test('render Markdown in preview mode', () => { const wrapper = shallow( <MarkdownEditor value="*Hello* Jest!" /> ); expect(wrapper).toMatchSnapshot(); wrapper.find('[name="toggle-preview"]').simulate('click'); expect(wrapper).toMatchSnapshot(); });
Иногда вам нужно взаимодействовать с элементом дочернего компонента, чтобы проверить эффект в вашем компоненте. Для этого вам понадобится правильный рендеринг DOM с помощью метода монтирования Enzyme:
test('open a code editor', () => { const wrapper = mount( <Playground code={code} /> ); expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0); wrapper.find('button').simulate('click'); expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1); });
Обработчики событий тестирования
Подобно тестированию событий, но вместо тестирования визуализированного вывода компонента с помощью моментального снимка используйте фиктивную функцию Jest для тестирования самого обработчика событий:
test('pass a selected value to the onChange handler', () => { const value = '2'; const onChange = jest.fn(); const wrapper = shallow( <Select items={ITEMS} onChange={onChange} /> ); expect(wrapper).toMatchSnapshot(); wrapper.find('select').simulate('change', { target: { value }, }); expect(onChange).toBeCalledWith(value); });
Не только JSX
Снимки Jest работают с JSON, поэтому вы можете протестировать любую функцию, возвращающую JSON, так же, как вы тестируете свои компоненты:
test('accept custom properties', () => { const wrapper = shallow( <Layout flexBasis={0} flexGrow={1} flexShrink={1} flexWrap="wrap" justifyContent="flex-end" alignContent="center" alignItems="center" /> ); expect(wrapper.prop('style')).toMatchSnapshot(); });
Отладка и устранение неполадок
Отладка неглубокого вывода средства рендеринга
Используйте метод отладки Enzyme для печати вывода поверхностного рендерера:
const wrapper = shallow(/*~*/); console.log(wrapper.debug());
Неудачные тесты с включенным покрытием
Когда ваши тесты терпят неудачу с флагом покрытия с diff следующим образом:
-<Button +<Component
Попробуйте заменить компонент стрелочной функции на обычную функцию:
- export default const Button = ({ children }) => { + export default function Button({ children }) {
Ошибка requestAnimationFrame
При запуске тестов вы можете увидеть такую ошибку:
console.error node_modules/fbjs/lib/warning.js:42
Warning: React depends on requestAnimationFrame. Make sure that you load a polyfill in older browsers. http://fb.me/react-polyfills
React 16 зависит от requestAnimationFrame
, поэтому вам нужно добавить в свои тесты полифилл:
// test/jestsetup.js
import 'raf/polyfill';
Ресурсы
- Шпаргалка шутки
- Тестирование приложений на React Макс Штойбер
- Миграция в шутку Кента К. Доддса
- Миграция авы в шутку Джейсона Брауна
Спасибо Крису Пойеру, Максу Штойберу и Анне Герус за вычитку и комментарии.
P. S. Посмотрите мой проект с открытым исходным кодом: React Styleguidist, генератор руководств по стилю компонентов с горячей перезагрузкой сервера разработки.
Подпишитесь на мою рассылку: https://tinyletter.com/sapegin
Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсудить рекламные и спонсорские возможности.
Чтобы узнать больше, прочтите нашу страницу о нас, поставьте лайк / напишите нам в Facebook или просто tweet / DM @HackerNoon.
Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!