Как протестировать компонент с помощью хуков react-redux?

У меня есть простой компонент Todo, который использует хуки react-redux, которые я тестирую с помощью фермента, но я получаю либо ошибку, либо пустой объект с поверхностным рендерингом, как указано ниже.

Как правильно тестировать компоненты с помощью хуков из react-redux?

Todos.js

const Todos = () => {
  const { todos } = useSelector(state => state);

  return (
    <ul>
      {todos.map(todo => (
        <li key = {todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

Todos.test.js v1

...

it('renders without crashing', () => {
  const wrapper = shallow(<Todos />);
  expect(wrapper).toMatchSnapshot();
});

it('should render a ul', () => {
  const wrapper = shallow(<Todos />);
  expect(wrapper.find('ul').length).toBe(1);
});

v1 Ошибка:

...
Invariant Violation: could not find react-redux context value; 
please ensure the component is wrapped in a <Provider>
...

Todos.test.js v2

...
// imported Provider from react-redux 

it('renders without crashing', () => {
  const wrapper = shallow(
    <Provider store = {store}>
      <Todos />
    </Provider>,
  );
  expect(wrapper).toMatchSnapshot();
});

it('should render a ul', () => {
  const wrapper = shallow(<Provider store = {store}><Todos /></Provider>);
  expect(wrapper.find('ul').length).toBe(1);
});

Тесты v2 также терпят неудачу, поскольку wrapper является <Provider>, и вызов dive() на wrapper вернет ту же ошибку, что и v1.

Заранее спасибо за вашу помощь!

Вы когда-нибудь догадывались об этом? Я столкнулся с той же проблемой после перехода на хуки Redux.

Markus Hay 04.07.2019 20:42

Похоже, это проблема конкретно с Enzyme, но пока не найдено адекватных обходных путей с использованием мелкого рендерера. Улучшенная поддержка хуков должна быть в следующем выпуске Enzyme: github.com/airbnb/enzyme/issues/2011

Markus Hay 04.07.2019 20:50

Используйте mount вместо мелкого, так как мелкое отображает только корневой компонент и размещает пользовательские дочерние компоненты как есть.

Shivang Gupta 16.04.2020 18:36
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
82
3
56 909
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Ответ принят как подходящий

Я мог бы протестировать компонент, который использует редукционные хуки, используя средство монтирования ферментов и предоставляя фиктивное хранилище провайдеру:

Компонент

import React from 'react';
import AppRouter from './Router'
import { useDispatch, useSelector } from 'react-redux'
import StartupActions from './Redux/Startup'
import Startup from './Components/Startup'
import './App.css';

// This is the main component, it includes the router which manages
// routing to different views.
// This is also the right place to declare components which should be
// displayed everywhere, i.e. sockets, services,...
function App () {
  const dispatch = useDispatch()
  const startupComplete = useSelector(state => state.startup.complete)

  if (!startupComplete) {
    setTimeout(() => dispatch(StartupActions.startup()), 1000)
  }

  return (
    <div className = "app">
      {startupComplete ? <AppRouter /> : <Startup />}
    </div>
  );
}

export default App;

Тестовое задание

import React from 'react';
import {Provider} from 'react-redux'
import { mount, shallow } from 'enzyme'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk';
import App from '../App';

const mockStore = configureMockStore([thunk]);

describe('App', () => {
  it('should render a startup component if startup is not complete', () => {
    const store = mockStore({
      startup: { complete: false }
    });
    const wrapper = mount(
      <Provider store = {store}>
        <App />
      </Provider>
    )
    expect(wrapper.find('Startup').length).toEqual(1)
  })
})

Кажется, что хуки Redux тесно связаны с компонентами Redux и React. И, обернув ваш компонент в Provider, разве вам не нужно было бы всегда проходить через него погружение()?

png 10.01.2020 00:59

Я удивлен, почему это принятый ответ. Это вообще не отвечает на вопрос, использование mount не является альтернативой мелкому.

omeralper 27.10.2020 21:19

@omeralper Так как возникает вопрос: «Как протестировать компонент с помощью хуков react-redux?» Я думаю, да, это отвечает на вопрос.

abidibo 25.11.2020 18:52

Есть другой способ, чем @abidibo, если вы используете селектор функций, определенный в другом файле. Вы можете смоделировать useSelector и свою функцию выбора, а затем использовать shallow из фермента:

Компонент

import * as React from 'react';
import { useSelector } from 'react-redux';
import Spinner from './Spinner';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';

const Example = () => {
  const isSpinnerDisplayed = useSelector(getIsSpinnerDisplayed);

  return isSpinnerDisplayed ? <Spinner /> : <Button />;
};

export default Example;

Селекторы

export const getIsSpinnerDisplayed = state => state.isSpinnerDisplayed;

Тестовое задание

import * as React from 'react';
import { shallow } from 'enzyme';
import Example from './Example';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';

jest.mock('react-redux', () => ({
  useSelector: jest.fn(fn => fn()),
}));
jest.mock('./selectors');

describe('Example', () => {
  it('should render Button if getIsSpinnerDisplayed returns false', () => {
    getIsSpinnerDisplayed.mockReturnValue(false);

    const wrapper = shallow(<Example />);

    expect(wrapper.find(Button).exists()).toBe(true);
  });
});

Это может быть немного хакерским, но это хорошо работает для меня :)

Кто-нибудь знает, как это сделать, используя Sinon вместо Jest? Я бился головой о стену, пытаясь заставить его работать так, как я думаю.

Matt McCormick 27.08.2020 20:52

это сработало и для меня:

import { shallow, mount } from "enzyme";
const store = mockStore({
  startup: { complete: false }
});

describe("==== Testing App ===== = ", () => {
  const setUpFn = props => {
    return mount(
      <Provider store = {store}>
        <App />
      </Provider>
    );
  };

  let wrapper;
  beforeEach(() => {
    wrapper = setUpFn();
  });

Вы разместили тот же ответ, что и основной.

pawelbylina 18.11.2019 18:15

Чтобы издеваться над useSelector, можно сделать это

import * as redux from 'react-redux'

const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue({ username:'test' })

Так просто. Моя ошибка заключалась в том, что я передал имя редуктора spy.mockReturnValue()

Lucas Andrade 24.03.2020 21:42

Лучший вариант имхо, работает без сложной настройки и подходит для любого хука

sam 12.05.2020 10:21

ИМО, самый простой путь к самому быстрому ответу. Это хорошо работает, если вы используете jest.each``

Neil 26.05.2020 12:05

как это будет работать, если компонент имеет несколько 'useSelector'r?

Aksana Tolstoguzova 01.09.2020 20:44

Может ли кто-нибудь продемонстрировать, как работать с несколькими useSelectors. Я только что начал с проекта реагирования на модульное тестирование. Пожалуйста, помогите!

Aashay Amballi 18.09.2020 17:22

Обходной путь для тестирования нескольких useSelector заключается в том, что вы можете создать хук со всеми селекторами, а затем протестировать этот пользовательский хук. Я знаю, что это не идеальное решение.

Kriti 28.09.2020 15:38

любое дополнительное решение для useDispatch?

Lars Holdaas 06.11.2020 06:41

@LarsHoldaas for useDispatch, вы можете попробовать что-то подобное, но я не совсем уверен в этом. const spyOnUseDispatch = jest.spyOn(redux, 'useDispatch') expect(spyOnUseDispatch).toHaveBeenCalled()

Kriti 01.03.2021 13:52

Это не работает с mount от Enzyme. Работает только для shallow рендеринга

Kitwradr 24.07.2021 20:24

Это МВА!

Fasani 06.08.2021 17:06

После поиска справки я объединил некоторые из найденных методов, чтобы издеваться над useSelector.

Сначала создайте функцию, которая выполняет некоторую загрузку перед тестом. Настройте хранилище с некоторыми значениями, которые вы хотите перезаписать, и смоделируйте функцию useSelector реакции-редукции.

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

import configureMockStore from 'redux-mock-store';
import * as Redux from 'react-redux';
import MyComponent from './MyComponent';

const mockSelectors = (storeValues) => {
  const mockStore = configureMockStore()({
    mobile: {
      isUserConnected: false
      ...storeValues
    },
  });

  jest
    .spyOn(Redux, 'useSelector')
    .mockImplementation(state => state.dependencies[0](mockStore.getState()));
};

describe('isUserConnected: true', () => {
    beforeEach(() => {
      mockSelectors({ isUserConnected: true });
      component = shallow(<MyComponent />);

      test('should render a disconnect button', () => {
         expect(component).toBeDefined();
         expect(component.find('button')).toBeTruthy();
      });
    });
  });

И компонент:

import React from 'react';
import { shallowEqual, useSelector } from 'react-redux';

const MyComponent = () => {    
  const isConnected = useSelector(selectIsConnected, shallowEqual);

  return (
    <>
      {
        showDisconnect ? (
          <button type = "button" onClick = {() => ()}>disconnect</button>
        ) : null
      }
    </>
  );
};

export default MyComponent;

Ниже код работает для меня.

import { configure, shallow} from 'enzyme'; 
import Adapter from 'enzyme-adapter-react-16'; 
import ServiceListingScreen  from './ServiceListingScreen'; 
import renderer from 'react-test-renderer';
import { createStore } from 'redux';
import serviceReducer from './../store/reducers/services'; 
import { Provider } from 'react-redux'
 
const store = createStore(serviceReducer  ) ; 
configure({adapter: new Adapter()}); 


const ReduxProvider = ({ children, reduxStore }) => (
  <Provider store = {reduxStore}>{children}</Provider>
)

describe('Screen/ServiceListingScreen', () => {
  it('renders correctly ', () => {
    const wrapper = shallow(<Provider store = {store}><ServiceListingScreen  /></Provider>);
    const tree = renderer.create(wrapper).toJSON();
    expect(tree).toMatchSnapshot();
  });
   
});

Я думаю, что это и лучший, и самый простой способ пошутить над хуком useSelector из магазина Redux:

  import * as redux from 'react-redux'

  const user = {
    id: 1,
    name: 'User',
  }

  const state = { user }

  jest
    .spyOn(redux, 'useSelector')
    .mockImplementation((callback) => callback(state))

Идея заключается в том, что вы можете издеваться только над подмножеством вашего магазина в state const.

Тестирование хуков React Redux с поверхностным рендерингом Enzyme

Прочитав все ответы здесь и покопавшись в документации, я захотел объединить способы тестирования компонентов React с использованием хуков react-redux с Enzyme и мелкого рендеринга.

Эти тесты основаны на имитации хуков useSelector и useDispatch. Я также приведу примеры как в Jest, так и в Sinon.

Базовый пример шутки

import React from 'react';
import { shallow } from 'enzyme';
import * as redux from 'react-redux';
import TodoList from './TodoList';

describe('TodoList', () => {
  let spyOnUseSelector;
  let spyOnUseDispatch;
  let mockDispatch;

  beforeEach(() => {
    // Mock useSelector hook
    spyOnUseSelector = jest.spyOn(redux, 'useSelector');
    spyOnUseSelector.mockReturnValue([{ id: 1, text: 'Old Item' }]);

    // Mock useDispatch hook
    spyOnUseDispatch = jest.spyOn(redux, 'useDispatch');
    // Mock dispatch function returned from useDispatch
    mockDispatch = jest.fn();
    spyOnUseDispatch.mockReturnValue(mockDispatch);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('should render', () => {
    const wrapper = shallow(<TodoList />);

    expect(wrapper.exists()).toBe(true);
  });

  it('should add a new todo item', () => {
    const wrapper = shallow(<TodoList />);

    // Logic to dispatch 'todoAdded' action

    expect(mockDispatch.mock.calls[0][0]).toEqual({
      type: 'todoAdded',
      payload: 'New Item'
    });
  });
});

Базовый пример синона

import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import * as redux from 'react-redux';
import TodoList from './TodoList';

describe('TodoList', () => {
  let useSelectorStub;
  let useDispatchStub;
  let dispatchSpy;  

  beforeEach(() => {
    // Mock useSelector hook
    useSelectorStub = sinon.stub(redux, 'useSelector');
    useSelectorStub.returns([{ id: 1, text: 'Old Item' }]);

    // Mock useDispatch hook
    useDispatchStub = sinon.stub(redux, 'useDispatch');
    // Mock dispatch function returned from useDispatch
    dispatchSpy = sinon.spy();
    useDispatchStub.returns(dispatchSpy);
  });

  afterEach(() => {
    sinon.restore();
  });
  
  // More testing logic...
});

Тестирование нескольких хуков useSelector

Тестирование нескольких useSelectors требует, чтобы мы имитировали состояние приложения Redux.

var mockState = {
  todos: [{ id: 1, text: 'Old Item' }]
};

Затем мы можем издеваться над собственной реализацией useSelector.

// Jest
const spyOnUseSelector = jest.spyOn(redux, 'useSelector').mockImplementation(cb => cb(mockState));

// Sinon
const useSelectorStub = sinon.stub(redux, 'useSelector').callsFake(cb => cb(mockState));

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

Mel Macaluso 27.06.2021 05:25

искал более простой способ смоделировать несколько useSelectors вместо того, чтобы писать его, используя .mockImplementationOnce n раз. спасибо @Andrew W

Maulik Sheth 05.08.2021 20:21

Вы можете попробовать redux-saga-test-plan крутое избыточное утверждение и работающую библиотеку, а также легкий вес, он автоматически выполняет всю вашу тяжелую работу по запуску саги и утверждению.

const mockState = { rick:"Genius", morty:"dumbAsss"}

expectSaga(yourCoolSaga)
      .provide({
        select({ selector }, next) {
          if (selector) {
            return mockState;
          }
          return next();
        }
      })
    // some assertion here
      .put(actions.updateRickMortyPowers(mockState))
      .silentRun();

Другие вопросы по теме