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