如何使用 react-redux 钩子测试组件?

Posted

技术标签:

【中文标题】如何使用 react-redux 钩子测试组件?【英文标题】:How to test a component using react-redux hooks? 【发布时间】:2019-11-11 14:47:18 【问题描述】:

我有一个简单的 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&lt;Provider&gt; 并且在 wrapper 上调用 dive() 将返回与 v1 相同的错误。

提前感谢您的帮助!

【问题讨论】:

你有没有想过这个问题?迁移到 Redux 挂钩后,我遇到了同样的问题。 这似乎是 Enzyme 的一个问题,但到目前为止,使用浅层渲染器似乎没有任何适当的解决方法。更好的钩子支持应该在下一个 Enzyme 版本中:github.com/airbnb/enzyme/issues/2011 使用 mount 而不是 shallow,因为 shallow 只渲染根组件并按原样放置自定义子组件 【参考方案1】:

要模拟 useSelector 使用可以做到这一点

import * as redux from 'react-redux'

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

【讨论】:

如此简单。我的错误是在 spy.mockReturnValue() 上传递减速器名称 IMO 的最佳选择,它无需任何复杂的设置即可工作,适用于每个挂钩 IMO,获得最快答案的最简单途径。如果您使用 jest.each``,这会很有效 如果一个组件有多个 'useSelecto'r 会如何工作? 谁能演示如何处理多个 useSelector。我刚刚开始进行单元测试反应项目。请有人帮忙!【参考方案2】:

我可以使用酶安装工具测试使用 redux 钩子的组件,并向 Provider 提供模拟存储:

组件

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 中,你不是总是需要通过它来潜水吗? 我很惊讶为什么这是公认的答案。这根本不能回答问题,使用 mount 不是浅层的替代品。 @omeralper 因为问题是“如何使用 react-redux 钩子测试组件?”我认为是的,它回答了这个问题。【参考方案3】:

如果您使用在另一个文件中定义的函数选择器,还有另一种方法而不是 @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来做到这一点?我一直在用头撞墙,试图让它像我认为的那样工作。【参考方案4】:

使用 Enzyme 的浅层渲染测试 React Redux Hooks

在阅读了此处的所有回复并深入阅读了文档之后,我想汇总使用带有 Enzyme 和浅层渲染的 react-redux 挂钩来测试 React 组件的方法。

这些测试依赖于模拟 useSelectoruseDispatch 钩子。我还将提供 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'
    );
  );
);

Sinon 基本示例

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 Hooks

测试多个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));

【讨论】:

这应该是公认的答案,因为它清楚地涵盖了如何有效地测试所需的钩子。 一直在寻找一种更简单的方法来模拟多个 useSelector,而不是使用 .mockImplementationOnce n 次来编写它。谢谢@Andrew W【参考方案5】:

下面的代码对我有用。

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();
  );
   
);

【讨论】:

【参考方案6】:

我认为这是在玩笑从 Redux 商店模拟 useSelector 钩子的最好和最简单的方法:

  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 中模拟商店的一个子集。

【讨论】:

【参考方案7】:

在寻求帮助后,我结合了我找到的一些方法来模拟 useSelector。

首先创建一个函数,在测试之前进行一些引导。 用一些你想覆盖和模拟 react-redux 的 useSelector 函数的值设置 store。

我认为它对于创建多个测试用例非常有用,您可以在其中看到存储状态如何影响组件的行为。

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;

【讨论】:

【参考方案8】:

你可以试试redux-saga-test-plancool redux 断言和运行库,轻量级的它可以自动完成所有繁重的运行 saga 和断言

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();

【讨论】:

【参考方案9】:

这对我也有用:

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();
  );

【讨论】:

您发布的答案与主要答案相同。

以上是关于如何使用 react-redux 钩子测试组件?的主要内容,如果未能解决你的问题,请参考以下文章

React-Redux - 钩子 API

如何在类组件中使用 react-redux useSelector?

如何对 React-Redux 连接组件进行单元测试?

如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?

Jest/Enzyme 单元测试:如何将存储传递给使用 redux 4 和 react-redux 6 连接功能的浅层组件

如何使用新的反应路由器钩子测试组件?