如何使用 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
是 <Provider>
并且在 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 组件的方法。
这些测试依赖于模拟 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'
);
);
);
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-plan
cool 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 useSelector?
如何正确使用带有 react-redux 的 useSelector 钩子的 curried 选择器函数?
Jest/Enzyme 单元测试:如何将存储传递给使用 redux 4 和 react-redux 6 连接功能的浅层组件