Тестирование функциональных компонентов, использующих useDispatch и useSelector, может немного отличаться. Вот как их проверить.
Тестирование функциональных компонентов, использующих useDispatch
и useSelector
ловушки, может немного отличаться от обычного тестирования подключенных компонентов. В этой статье демонстрируется надежный способ тестирования компонентов, который работает для обоих типов компонентов (компонентов, которые используют эти перехватчики или connected
компонентов).
Поскольку лучший способ чему-то научиться - это создать небольшой проект, мы создадим крошечное веб-приложение. Сверху будет отображаться текст количество отсчетов и кнопка, которая увеличивает количество отсчетов при нажатии. Полный исходный код этого руководства доступен здесь: https://github.com/AngSin/counter-app-tested.
Во-первых, давайте настроим наш проект с помощью приложения create-react-app. Если у вас нет приложения create-response-app, вы можете загрузить его, используя: npm i -g create-react-app
. После установки инициализируйте новый проект, используя: create-react-app <PROJECT_NAME>
.
Затем добавить сокращение: yarn add redux react-redux
.
Мы будем тестировать, используя enzyme
в качестве нашей тестовой библиотеки: yarn add -D enzyme enzyme-adapter-react-16
.
Вот основные файлы скелета проекта:
Индекс
// index.js import React from ‘react’; import ReactDOM from ‘react-dom’; import { Provider } from ‘react-redux’; import store from ‘./store’; import Component from ‘./Component’; import ‘./index.css’; ReactDOM.render( <Provider store={store}><Component /></Provider>, document.getElementById(‘root’) );
Компонент
// Component.js import React from 'react'; import { useDispatch, useSelector } from "react-redux"; import './Component.css'; import { addCount } from './actions'; const Component = () => { const count = useSelector(state => state.count); const dispatch = useDispatch(); const handleClick = () => dispatch(addCount()); return ( <div className="App"> <h3> Count: {count} </h3> <button onClick={handleClick}> Increase count </button> </div> ); }; export default Component;
Действие
// actions.js export const ADD_COUNT_TYPE = 'ADD_COUNT_TYPE'; export const addCount = () => ({ type: ADD_COUNT_TYPE });
Редуктор:
// reducer.js import { ADD_COUNT_TYPE } from './actions'; const initialState = { count: 0, }; export default(state = initialState, action) => { switch(action.type) { case ADD_COUNT_TYPE: return {...state, count: state.count + 1}; default: return state; } };
Теперь, чтобы создать набор тестов, который работает с тестовым скриптом create-react-app, давайте создадим тестовый файл, который следует соглашению об именах test.js
. Я назвал свой Component.test.js
. Поскольку мы используем фермент, нам также необходимо настроить фермент в нашем тестовом файле:
// Component.test.js import Enzyme, { mount } from 'enzyme'; import EnzymeAdapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new EnzymeAdapter() });
Обычно этот код помещается в файл testSetup.js
, но поскольку я хотел сделать этот проект простым, я не следовал многим соглашениям о структуре реактивных папок.
Мы можем визуализировать наш компонент в тестовом dom, используя функции enzyme
shallow
или mount
, но поскольку в этом случае наш компонент зависит от хранилища redux, мы также должны заключить его в Provider
HOC, экспортированный react-redux
. И поскольку цель этого руководства - полностью протестировать наш компонент, включая сторону redux, мы должны создать фиктивное хранилище для нашего redux <Provider />
с начальным состоянием, которое удовлетворяет структуре нашего редуктора. Как только это будет сделано, мы сможем визуализировать наш компонент с помощью mount
. Теперь мы можем проверить и убедиться, что количество отсчетов отображается правильно, а при нажатии кнопки обновляется и количество отображаемых отсчетов. Наш файл должен выглядеть так:
// Component.test.js import React from 'react'; import Enzyme, { mount } from 'enzyme'; import EnzymeAdapter from 'enzyme-adapter-react-16'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import Component from './Component'; import reducer from './reducer'; Enzyme.configure({ adapter: new EnzymeAdapter() }); describe('<Component /> unit test', () => { const mockStore = createStore(reducer, {count: 0}); const getWrapper = () => mount( <Provider store={mockStore}> <Component/> </Provider> ); it('should add to count and display the correct # of counts', () => { const wrapper = getWrapper(); expect(wrapper.find('h3').text()).toEqual('Count: 0'); wrapper.find('button').simulate('click'); expect(wrapper.find('h3').text()).toEqual('Count: 1'); }); });
Теперь наш компонент протестирован правильно. Обычно это лучший способ протестировать компоненты, моделируя действия, а не проверяя свойства / состояния компонентов или проверяя, были ли запущены определенные функции. Однако давайте сделаем еще один шаг.
Что, если при нажатии кнопки мы выполняем HTTP-вызов на наш сервер, чтобы обновить количество лайков к комментарию? В этом случае простой проверки пользовательского интерфейса будет недостаточно. Нам нужно будет проверить, отправлено ли соответствующее действие, которое позаботится о выполнении асинхронного запроса. В этом примере мы не выполняем асинхронный запрос, а отправляем действие для обновления количества счетчиков. Мы можем написать тест, чтобы убедиться, что действие отправлено. Теперь наш файл будет выглядеть так:
// Component.test.js import React from 'react'; import Enzyme, { mount } from 'enzyme'; import EnzymeAdapter from 'enzyme-adapter-react-16'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import Component from './Component'; import { addCount } from './actions'; import reducer from './reducer'; Enzyme.configure({ adapter: new EnzymeAdapter() }); describe('<Component /> unit test', () => { const getWrapper = (mockStore = createStore(reducer, { count: 0 })) => mount( <Provider store={mockStore}> <Component/> </Provider> ); it('should add to count and display the correct # of counts', () => { const wrapper = getWrapper(); expect(wrapper.find('h3').text()).toEqual('Count: 0'); wrapper.find('button').simulate('click'); expect(wrapper.find('h3').text()).toEqual('Count: 1'); }); it('should dispatch the correct action on button click', () => { const mockStore = createStore(reducer, { count: 0 }); mockStore.dispatch = jest.fn(); const wrapper = getWrapper(mockStore); wrapper.find('button').simulate('click'); expect(mockStore.dispatch).toHaveBeenCalledWith(addCount()); }); });
Что мы здесь делаем? Вместо инициализации mockStore
в начале блока описания мы передаем его в качестве аргумента функции getWrapper
. Это позволяет нам иметь ссылку на объект mockStore
и проверять, был ли вызван его метод dispatch
. Кроме того, если приложение сложное, мы также можем проверить, что не только было отправлено действие, но и было ли оно правильным:
expect(mockStore.dispatch).toHaveBeenCalledWith(addCount());
Очевидно, что в нашем случае второй тест избыточен, потому что его единственная цель - обновить пользовательский интерфейс. Однако, если вместо обновления пользовательского интерфейса был задействован побочный эффект, например вызов HTTP / HTTPS API, этот второй тест был бы очень полезен.
Надеюсь, вы нашли эту статью полезной и сможете лучше использовать ее для тестирования своих компонентов. Если у вас возникнут какие-либо проблемы, вы можете оставить комментарий ниже или открыть проблему в репозитории с исходным кодом: https://github.com/AngSin/counter-app-tested. Если вы программист-самоучка, вам может пригодиться эта статья.