Шутка, перезагрузил

TL;DRЕсли вы вообще не тестируете свои компоненты React (Native) или полагаетесь на недостаточные высокоуровневые дымовые тесты, вы не одиноки. Написание этих модульных тестов должно быть одним из наименее полезных занятий для разработчика. Но подождите: снимки спешат на помощь! Новейшая версия Jest включает в себя тестирование моментальных снимков — и вполне может изменить ваше мнение.

Последний релиз Jest вызвал настоящий ажиотаж, и это понятно: одна только его функция тестирование снимками просто сводит с ума. Если вы похожи на меня, возможно, вы не писали модульные тесты для своих компонентов React (Native) по ряду веских причин:

  • "Их сложно писать"
  • "Они хрупкие"
  • "Они не приносят такой пользы, если у вас есть хотя бы несколько интеграционных тестов"

Однако, начиная с Jest 14, вам, возможно, придется пересмотреть эти аргументы. Снимки просты в использовании, их легко обновлять, и они обеспечивают полный охват ваших компонентов, включая динамическое поведение. Как они работают? Даже это можно объяснить всего двумя пунктами:

  1. При первом рендеринге компонента тестом делается снимок и сохраняется в файл.
  2. При повторном запуске новый рендер сравнивается со снимком. Если есть несоответствие, вы выбираете, какое из них оставить (и, возможно, исправить ваше приложение).

Кроме того, Jest предоставляет комплексную среду тестирования, хорошую производительность, первоклассные различия и контекст ошибок, а также много вкусностей CLI. Даже результаты покрытия из коробки! Неудивительно, что он получил восторженные отзывы.

Снимки еще не пробовали? Давайте дадим им спину. Следующие примеры основаны на Mady, приложении для интернационализации программного обеспечения. До сих пор у Mady не было абсолютно никаких тестов, посвященных компонентам React.

Компонент hello-world

Сначала мы попробуем моментальные снимки на очень простом компоненте. Header Мэди — это функциональный компонент без сохранения состояния, показывающий имя приложения и пару кнопок. Множество стилей для тестирования, но не более того.

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

/* eslint-env jest */ 
import React from 'react'; 
import renderer from 'react-test-renderer'; 
import Header from '../header'; 
it('renders correctly', () => {
  const tree = renderer.create(<Header />).toJSON();
  expect(tree).toMatchSnapshot();
});

Запустите Jest, и будет создан новый снимок:

exports[`renders correctly 1`] = `
<div
  style={
    Object {
      "WebkitFlex": "0 0 2.5em",
      "alignItems": "center",
      "backgroundColor": "#dcd6ff",
      "display": "flex",
      "flex": "0 0 2.5em",
      "flexDirection": "row",
      "padding": "5px 8px"
    }
  }>
  ...
</div>
`;

Это все, что нужно сделать, и он будет охватывать каждое маленькое свойство CSS, которое вы можете использовать. Изменить цвет? Шутка предупредит вас. Сдвинуть заголовок на один пиксель вниз? Предупреждение снова. Скажите Jest обновить снимок для вас, и все готово.

Примечание: в текущей версии React вам может понадобиться добавить это в начале: jest.mock(‘react-dom’).

Насмешки над дочерними компонентами

Теперь давайте протестируем чуть более сложный компонент. TranslatorRow Мэди отображает исходное сообщение для перевода, а также несколько переводов. Но сами переводы, которые можно редактировать, обрабатываются дочерним компонентом Translation.

Мы пишем модульные тесты для TranslatorRow, поэтому мы не хотим, чтобы переводы также отображались (они сами будут иметь модульные тесты). Но мы действительно хотим знать, какие реквизиты им передаются.

Для этого пригодится функция насмешек Jest. Давайте используем его для перевода:

jest.mock('../translation', () => jest.fn((props) =>
  <div dataMockType="Translation" {...props} />
));

Обратите внимание, как нам удается включить в снимок передаваемые реквизиты: {…props}. Обратите также внимание на атрибут dataMockType, который я считаю полезным для простой идентификации фиктивных компонентов в моментальном снимке.

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

import TranslatorRow from '../translatorRow';

const MSG_WITH_TRANSLATIONS = {
  id: 'msgId',
  text: 'A message',
  translations: [
    { id: 'id1', lang: 'es', translation: 'Un mensaje' },
    { id: 'id2', lang: 'ca', translation: 'Un missatge' },
  ],
};

it('renders correctly a message with translations', () => {
  const tree = renderer.create(
    <TranslatorRow
      msg={MSG_WITH_TRANSLATIONS}
      langs={['es', 'ca']}
    />
  ).toJSON();
  expect(tree).toMatchSnapshot();
});

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

...
<div
  dataMockType="Translation"
  lang="es"
  msg={...}
  translation={
    Object {
      "id": "id1",
      "lang": "es",
      "translation": "Un mensaje"
    }
  } />
...
<div
  dataMockType="Translation"
  lang="ca"
  msg={...}
  translation={
    Object {
      "id": "id2",
      "lang": "ca",
      "translation": "Un missatge"
    }
  } />

Контейнеры ретрансляции моментальных снимков

В реальном приложении TranslatorRow является контейнером Relay, поэтому ожидается, что ему будет передан реквизит relay. Теперь Relay Higher-Order Component (HOC) находится за пределами нашего приложения, и мы можем даже не знать полную форму этой опоры.

Я нашел более простым просто исключить HOC из нашего модульного теста. Для этой цели наш экспорт по умолчанию в translatorRow.js — это упакованный TranslatorRow, но мы также предоставляем развернутую версию как именованный экспорт.

const fragments = {
  msg: () => Relay.QL`
    fragment on Message {
      id
      text
      ${Translation.getFragment('msg')}
      translations(first: 100000) { edges { node {
        id
        lang
        ${Translation.getFragment('translation')}
      }}}
    }
  `,
};

class TranslatorRow extends React.PureComponent {
  // ...
}

export default Relay.createContainer(TranslatorRow, { fragments });
export { TranslatorRow as _TranslatorRow };  // just for unit tests!

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

import { _TranslatorRow as TranslatorRow } from '../translatorRow';

const MSG_WITH_TRANSLATIONS = {
  id: 'msgId',
  text: 'A message',
  translations: { edges: [
    { node: {
      { id: 'id1', lang: 'es', translation: 'Un mensaje' },
    } },
    { node: {
      { id: 'id2', lang: 'ca', translation: 'Un missatge' },
    } },
  ] },
};

// ...

Тестирование динамического поведения с помощью снимков

Компоненты React могут по-разному реагировать на взаимодействие с пользователем. Один компонент может обновить свое состояние и выполнить повторный рендеринг, тогда как другой может просто вызвать функцию, переданную в качестве реквизита.

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

Динамические тесты на самоотрисовывающемся компоненте

В этом примере TranslatorRow можно навести и повторно отобразить при mouseEnter и mouseLeave:

class TranslatorRow extends React.PureComponent {
  constructor() {
    super();
    this.state = { hovered: false };
  }

  render() {
    return (
      <div
        className={this.state.hovered ? 'hovered' : ''}
        onMouseEnter={() => this.setState({ hovered: true })}
        onMouseLeave={() => this.setState({ hovered: false })}
      >
        {/* ... */}
      </div>
    );
  }
}

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

it('changes its class upon mouseEnter/mouseLeave', () => {
  const component = renderer.create(
    <TranslatorRow
      msg={MSG_WITH_TRANSLATIONS}
      langs={['es', 'ca']}
    />
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot(); // snapshot 1

  tree.props.onMouseEnter();
  tree = component.toJSON();      // re-renders
  expect(tree).toMatchSnapshot(); // snapshot 2

  tree.props.onMouseLeave();
  tree = component.toJSON();      // re-renders
  expect(tree).toMatchSnapshot(); // snapshot 3
});

Jest сохранит не один, а три снимка для этого теста, фиксируя все детали того, как компонент должен отображаться в состоянии ожидания и при наведении курсора.

Динамические тесты с реквизитами обратного вызова

Во втором примере TranslatorRow реагирует на события щелчка, вызывая свойство onClick с идентификатором сообщения:

class TranslatorRow extends React.PureComponent {
  render() {
    const { onClick, msg } = this.props;
    return (
      <div onClick={() => onClick(msg.id)}>
        {/* ... */}
      </div>
    );
  }
}

Чтобы проверить это поведение, мы используем фиктивные (или шпионские) функции Jest:

it('reacts correctly to click events', () => {
  const spyOnClick = jest.fn();
  const tree = renderer.create(
    <TranslatorRow
      msg={MSG_WITH_TRANSLATIONS}
      langs={['es', 'ca']}
      onClick={spyOnClick}
    />
  ).toJSON();
  tree.props.onClick();
  expect(spyOnClick).toBeCalledWith(MSG_WITH_TRANSLATIONS.id);
});

Хорошо, мы вообще не используем моментальные снимки в этом примере, но я включил их, чтобы дать вам более широкую картину.

Выводы

Это лишь малая часть того, что может сделать Jest. После периода, когда многие считали его сложным в использовании и ограниченным собственными вариантами использования Facebook, Крис Поджер и другие отлично переписали его. Это не просто шумиха: устройте тест-драйв!

Я надеюсь, что этот пост дал вам некоторые идеи, чтобы, наконец, заняться теми девственными областями вашего проекта, которые никогда не видели теста. Шепну вам на ухо: тестирование моментальных снимков может даже… быть веселым!

Первоначально опубликовано на Math.random() 27 сентября 2016 г.