javascript

Модульное тестирование react компонетнов withRouter (jest, enzyme)

  • вторник, 6 марта 2018 г. в 03:14:51
https://habrahabr.ru/post/350504/
  • Разработка веб-сайтов
  • Веб-дизайн
  • ReactJS
  • Node.JS
  • JavaScript


При разработке модульных тестов для react компонента, обернутого в вызов withRouter(Component) столкнулся с сообщением об ошибке, что такой компонент может существовать только в контексте роутера. Решение этой проблемы очень простое и не должно по идее вызывать вопрсов. Хотя почему-то ссылки на документацию https://reacttraining.com/react-router/web/guides/testing Google упорно отказывался выдавать. Меня это совсем не удивляет, т.к. документация написано как чистое SPA-приложение без всякого там SSR и с точки зрения поисковой машины выглядит вот так:

Показать изображение
image

Кому достаточно документации может на этом закончить чтение. А для себя я сделаю несколько заметок под катом.

Тестируемый компонент (paginator) принимает в качестве параметров количество строк — всего, на странице и номер текущей страницы. Необходимо сформировать компонент со ссылками вида:

  • / или /my/base/url — для первой страницы
  • /page/{n} или /my/base/url/page/{n} — для остальных страниц
  • для одностраничных документов компонент не формировать

import React from 'react';
import _ from 'lodash';
import { withRouter } from 'react-router-dom';
import Link from '../asyncLink'; // eslint-disable-line

function prepareLink(match, page) {
  const { url } = match;
  const basePath = url.replace(/\/(page\/[0-9]+)?$/, '');
  if (page === 1) {
    return basePath || '/';
  }
  return `${basePath}/page/${page}`;
}

const Pagination = ({ count, pageLength, page, match }) => ( // eslint-disable-line react/prop-types, max-len
  count && pageLength && count > pageLength
    ?
      <nav>
        <ul className="pagination">
          {
            _.range(1, 1 + Math.ceil(count / pageLength)).map(index => (
              <li className={`page-item${index === page ? ' active' : ''}`} key={index}>
                <Link className="page-link" to={prepareLink(match, index)}>
                  {index}
                </Link>
              </li>))
          }
        </ul>
      </nav>
    : null
);

export default withRouter(Pagination);

В этом компоненте используется свойство match, которое становится доступным только для компонентов, обернутых в вызов withRouter(Pagination). При тестировании нужно создать контекст при помощи специального роутера для тестирвлоани — MemoryRouter. А так же поместить компонент в соотвтествующий роут Route для формирования matches:

/* eslint-disable no-undef, function-paren-newline */
import React from 'react';
import { MemoryRouter, Route } from 'react-router-dom';
import { configure, mount } from 'enzyme';
import renderer from 'react-test-renderer';
import Adapter from 'enzyme-adapter-react-16';
import Pagination from '../../../src/react/components/pagination';

configure({ adapter: new Adapter() });

test('Paginator snapshot', () => {
  const props = {
    count: 101,
    pageLength: 10,
    page: 10,
  };
  const component = renderer.create(
    <MemoryRouter initialEntries={['/', '/page/10', '/next']} initialIndex={1}>
      <Route path="/page/:page">
        <Pagination {...props} />
      </Route>
    </MemoryRouter>,
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

test('Paginator for root route 1st page', () => {
  const props = {
    count: 101,
    pageLength: 10,
    page: 1,
  };
  const component = mount(
    <MemoryRouter initialEntries={['/before', '/', '/next']} initialIndex={1}>
      <Route path="/">
        <Pagination {...props} />
      </Route>
    </MemoryRouter>,
  );
  expect(component.find('li.active').find('a').prop('href')).toEqual('/');
  expect(component.find('li').first().find('a').prop('href')).toEqual('/');
});

test('Paginator for root route 2nd page', () => {
  const props = {
    count: 101,
    pageLength: 10,
    page: 2,
  };
  const component = mount(
    <MemoryRouter initialEntries={['/', '/page/2', '/next']} initialIndex={1}>
      <Route path="/page/:page">
        <Pagination {...props} />
      </Route>
    </MemoryRouter>,
  );
  expect(component.find('li.active').find('a').prop('href')).toEqual('/page/2');
  expect(component.find('li').first().find('a').prop('href')).toEqual('/');
});


test('Paginator for some route 1st page', () => {
  const props = {
    count: 101,
    pageLength: 10,
    page: 1,
  };
  const component = mount(
    <MemoryRouter initialEntries={['/', '/some', '/next']} initialIndex={1} context={{}}>
      <Route path="/some">
        <Pagination {...props} />
      </Route>
    </MemoryRouter>,
  );
  expect(component.find('li.active').find('a').prop('href')).toEqual('/some');
  expect(component.find('li').first().find('a').prop('href')).toEqual('/some');
});

test('Paginator for /some route 2nd page', () => {
  const props = {
    count: 101,
    pageLength: 10,
    page: 2,
  };
  const component = mount(
    <MemoryRouter initialEntries={['/', '/some/page/2', '/next']} initialIndex={1}>
      <Route path="/some/page/:page">
        <Pagination {...props} />
      </Route>
    </MemoryRouter>,
  );
  expect(component.find('li.active').find('a').prop('href')).toEqual('/some/page/2');
  expect(component.find('li').first().find('a').prop('href')).toEqual('/some');
});

MemoryRouter содержит «воображаемую» историю компонента, по которой можно будет потом двигаться вперед и назад. Начальный индекс задается свойством initialIndex.

Тесты используют популяртную библиотеку от airbnb — enzyme и запусткаются командой jest.
Фреймверк jest при первом запуске формирует моментальный снимок компонента, с которым потом сверяет получаемый в результате рентеринга документ:

  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();

Команды библиотеки enzyme для поиска элементов DOM и их анализа выглядят не так лаконично как например jquery. Тем не менее все очень удобно.

apapacy@gmail.com
5 марта 2018 года.