Август 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';

Ресурсы

Спасибо Крису Пойеру, Максу Штойберу и Анне Герус за вычитку и комментарии.

P. S. Посмотрите мой проект с открытым исходным кодом: React Styleguidist, генератор руководств по стилю компонентов с горячей перезагрузкой сервера разработки.

Подпишитесь на мою рассылку: https://tinyletter.com/sapegin

Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсудить рекламные и спонсорские возможности.

Чтобы узнать больше, прочтите нашу страницу о нас, поставьте лайк / напишите нам в Facebook или просто tweet / DM @HackerNoon.

Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!