【问题标题】:How to test a component using react-redux hooks?如何使用 react-redux 钩子测试组件?
【发布时间】: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 只渲染根组件并按原样放置自定义子组件

标签: reactjs testing redux react-redux enzyme


【解决方案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();
            });
          

          【讨论】:

          • 您发布的答案与主要答案相同。
          猜你喜欢
          • 2021-05-27
          • 1970-01-01
          • 2020-03-03
          • 2020-04-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-10-30
          • 1970-01-01
          相关资源
          最近更新 更多