Тестирование функциональных компонентов, использующих 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. Если вы программист-самоучка, вам может пригодиться эта статья.