开玩笑地模拟 useDispatch 并在功能组件中使用该调度操作来测试参数
Posted
技术标签:
【中文标题】开玩笑地模拟 useDispatch 并在功能组件中使用该调度操作来测试参数【英文标题】:mock useDispatch in jest and test the params with using that dispatch action in functional component 【发布时间】:2020-03-19 22:22:50 【问题描述】:您好,我正在使用笑话和酶编写功能组件测试。当我模拟单击时,组件的参数(使用 useState 的组件状态)会发生变化。并且当状态发生更改时, useEffect 调用并在 useEffect 中,我在更改后使用参数调度一些异步操作。所以我想测试参数,我正在调度动作。为此,我想模拟调度。我怎样才能做到这一点? 任何人都可以帮助我,在此先感谢。下面我分享代码。
component.js
import React, useEffect, useState from 'react';
import PropTypes from 'prop-types';
import useSelector, useDispatch from 'react-redux';
import useTranslation from 'react-i18next';
import clientOperations, clientSelectors from '../../store/clients';
import Breadcrumb from '../../components/UI/Breadcrumb/Breadcrumb.component';
import DataTable from '../../components/UI/DataTable/DataTable.component';
import Toolbar from './Toolbar/Toolbar.component';
const initialState =
search: '',
type: '',
pageNo: 0,
rowsPerPage: 10,
order: 'desc',
orderBy: '',
paginated: true,
;
const Clients = ( history ) =>
const t = useTranslation();
const dispatch = useDispatch();
const totalElements = useSelector(state => state.clients.list.totalElements);
const records = useSelector(clientSelectors.getCompaniesData);
const [params, setParams] = useState(initialState);
useEffect(() =>
dispatch(clientOperations.fetchList(params));
, [dispatch, params]);
function updateParams(newParams)
setParams(state => (
...state,
...newParams,
));
function searchHandler(value)
updateParams(
search: value,
pageNo: 0,
);
function typeHandler(event)
updateParams(
type: event.target.value,
pageNo: 0,
);
function reloadData()
setParams(initialState);
const columns =
id: t('CLIENTS_HEADING_ID'),
name: t('CLIENTS_HEADING_NAME'),
abbrev: t('CLIENTS_HEADING_ABBREV'),
;
return (
<>
<Breadcrumb items=[ title: 'BREADCRUMB_CLIENTS' ]>
<Toolbar
search=params.search
setSearch=searchHandler
type=params.type
setType=typeHandler
reloadData=reloadData
/>
</Breadcrumb>
<DataTable
rows=records
columns=columns
showActionBtns=true
deletable=false
editHandler=id => history.push(`/clients/$id`)
totalElements=totalElements
params=params
setParams=setParams
/>
</>
);
;
Component.test.js
const initialState =
clients:
list:
records: companies,
totalElements: 5,
,
,
fields:
companyTypes: ['All Companies', 'Active Companies', 'Disabled Companies'],
,
;
const middlewares = [thunk];
const mockStoreConfigure = configureMockStore(middlewares);
const store = mockStoreConfigure( ...initialState );
const originalDispatch = store.dispatch;
store.dispatch = jest.fn(originalDispatch)
// configuring the enzyme we can also configure using Enjym.configure
configure( adapter: new Adapter() );
describe('Clients ', () =>
let wrapper;
const columns =
id: i18n.t('CLIENTS_HEADING_ID'),
name: i18n.t('CLIENTS_HEADING_NAME'),
abbrev: i18n.t('CLIENTS_HEADING_ABBREV'),
;
beforeEach(() =>
const historyMock = push: jest.fn() ;
wrapper = mount(
<Provider store=store>
<Router>
<Clients history=historyMock />
</Router>
</Provider>
);
);
it('on changing the setSearch of toolbar should call the searchHandler', () =>
const toolbarNode = wrapper.find('Toolbar');
expect(toolbarNode.prop('search')).toEqual('')
act(() =>
toolbarNode.props().setSearch('Hello test');
);
toolbarNode.simulate('change');
****here I want to test dispatch function in useEffect calls with correct params"**
wrapper.update();
const toolbarNodeUpdated = wrapper.find('Toolbar');
expect(toolbarNodeUpdated.prop('search')).toEqual('Hello test')
)
);
【问题讨论】:
我可以调用 store.dispatch 吗?在 React 组件中直接与 store 交互是一种反模式,无论是显式导入 store 还是通过 context 访问它(有关更多详细信息,请参阅关于 store setup 的 Redux FAQ 条目)。让 React Redux 的 connect 处理对 store 的访问,并使用它传递给 props 的 dispatch 来 dispatch action。 @sid7747 这就是使用 mapStateToProps 和 mapDispatch 的方法。使用钩子可以将它们直接放入组件中。在使用这些之后,我倾向于使用中间连接层或中间数据组件使用钩子,因此更容易单独测试。 【参考方案1】:[upd] 从那以后我的想法发生了巨大的变化。现在我认为模拟存储(使用redux-mock-store
甚至改变其状态的真实存储) - 并用<Provider store=mockedStore>
包装组件 - 更加可靠和方便。检查下面的另一个答案。
如果您模拟react-redux
,您将能够验证useDispatch
调用的参数。同样在这种情况下,您将需要重新创建useSelector
的逻辑(这真的很简单,实际上您不必让 mock 成为一个钩子)。同样使用这种方法,您根本不需要模拟商店或<Provider>
。
import useSelector, useDispatch from 'react-redux';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => (
useSelector: jest.fn(),
useDispatch: () => mockDispatch
));
it('loads data on init', () =>
const mockedDispatch = jest.fn();
useSelector.mockImplementation((selectorFn) => selectorFn(yourMockedStoreData));
useDispatch.mockReturnValue(mockedDispatch);
mount(<Router><Clients history=historyMock /></Router>);
expect(mockDispatch).toHaveBeenCalledWith(/*arguments your expect*/);
);
【讨论】:
我已经尝试过您的建议,但我无法使用它安装组件。给出错误 TypeError: _reactRedux.useSelector.mockImplementation is not a function 您是否将jest.mock(...
和import ... from 'react-redux'
都放入了测试文件?
是的,我两个都放了。
增加了对 mocks 使用 factoring 回调的描述
jest.mock('react-redux'); useDispatch.mockImplementation(() => store.dispatch) useSelector.mockImplementation(() => ); it('loads data on init', () => const historyMock = push: jest.fn() ; let wrapper; wrapper = mount(<Router><Clients history=historyMock /></Router>); expect(wrapper).toMatchSnapshot(); );
使用这个 react-redux 是模拟的,但是如何从 useSelector 提供数据?如果我不使用这种方法来模拟 useDispatch.mockImplementation(() => store.dispatch) 那么它给出的错误就像调度不是一个函数。【参考方案2】:
import * as redux from "react-redux";
describe('dispatch mock', function()
it('should mock dispatch', function()
//arrange
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const mockDispatchFn = jest.fn()
useDispatchSpy.mockReturnValue(mockDispatchFn);
//action
triggerYourFlow();
//assert
expect(mockDispatchFn).toHaveBeenCalledWith(expectedAction);
//teardown
useDispatchSpy.mockClear();
)
);
从功能组件中,我们像上面那样模拟调度以阻止它执行真正的实现。希望对您有所帮助!
【讨论】:
【参考方案3】:这就是我使用反应测试库解决的方法:
我有这个包装器来用 Provider 渲染组件
export function configureTestStore(initialState = )
const store = createStore(
rootReducer,
initialState,
);
const origDispatch = store.dispatch;
store.dispatch = jest.fn(origDispatch)
return store;
/**
* Create provider wrapper
*/
export const renderWithProviders = (
ui,
initialState = ,
initialStore,
renderFn = render,
) =>
const store = initialStore || configureTestStore(initialState);
const testingNode =
...renderFn(
<Provider store=store>
<Router history=history>
ui
</Router>
</Provider>
),
store,
;
testingNode.rerenderWithProviders = (el, newState) =>
return renderWithProviders(el, newState, store, testingNode.rerender);
return testingNode;
使用它,我可以从测试内部调用store.dispatch
,并检查它是否被我想要的操作调用。
const mockState =
foo: ,
bar:
const setup = (props = ) =>
return ...renderWithProviders(<MyComponent ...props />, mockState)
;
it('should check if action was called after clicking button', () =>
const getByLabelText, store = setup();
const acceptBtn = getByLabelText('Accept all');
expect(store.dispatch).toHaveBeenCalledWith(doActionStuff("DONE"));
);
【讨论】:
【参考方案4】:import * as ReactRedux from 'react-redux'
describe('test', () =>
it('should work', () =>
const mockXXXFn = jest.fn()
const spyOnUseDispatch = jest
.spyOn(ReactRedux, 'useDispatch')
.mockReturnValue( xxxFn: mockXXXFn )
// Do something ...
expect(mockXXXFn).toHaveBeenCalledWith(...)
spyOnUseDispatch.mockRestore()
)
)
更新:不要使用与 Redux 存储实现逻辑强耦合的 React Redux hooks API,这使得测试变得非常困难。
【讨论】:
【参考方案5】:我看到了使用实际<Provider store=store>
的优势:
useDispatch
和 useSelector
)
但是对于单元测试来说,引入真正的 store 和真正的 reducer 和真正的调度对我来说似乎有点矫枉过正(但可以用于集成测试):
模拟所有服务器请求可能是一项艰巨的任务 通常我们已经在每个切片的基础上测试了该逻辑考虑到这一点,从redux-mock-store
中选择了configureStore
而不是redux
并得到了下一个助手(使用酶):
import act from 'react-dom/test-utils';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import mount from 'enzyme';
import Provider from 'react-redux';
function renderInRedux(
children,
initialData =
)
let state = initialData;
const store = (configureMockStore([thunk]))(() => state);
const wrapper = mount(
<Provider store=store>
children
</Provider>
);
return
/*
since Enzyme wrappers are readonly, we need retrieve target element in unit test each time after any interaction
*/
getComponent()
return wrapper.childAt(0);
,
/*
set store to any desired config; previous value is replaced;
*/
replaceStore(newState)
act(() =>
state = newState;
store.dispatch( type: dummyActionTypeName ); // just to trigger listeners
);
wrapper.update();
,
/*
bridge to redux-mock-store's getActions
*/
getActions()
return store.getActions().filter(( type ) => type !== dummyActionTypeName);
,
/*
bridge to redux-mock-store's clearActions()
*/
clearActions()
return store.clearActions();
,
;
及用法示例:
const
getComponent,
replaceStore,
= renderInRedux(<Loader />, isRequesting: false );
expect(getComponent().isEmptyRender()).toBeTruthy();
replaceStore( isRequesting: true );
expect(getComponent().isEmptyRender()).toBeFalsy();
但是,如果我们想测试调度,如何避免模拟服务器端交互?好吧,它本身并没有。但是我们可以轻松地模拟和测试动作调度:
import saveThing as saveThingAction from '../myActions.js';
jest.mock('../myActions.js', () => (
saveThing: jest.fn().mockReturnValue( type: 'saveThing' )
));
beforeEach(() =>
);
....
const getComponent, getActions = renderInRedux(
<SomeForm />,
someMockedReduxStore
);
getComponent().find(Button).simulate('click');
expect(getActions()).toContainEqual(saveThingAction());
expect(saveThingAction).toHaveBeenCalledWith(someExpectedArguments);
【讨论】:
以上是关于开玩笑地模拟 useDispatch 并在功能组件中使用该调度操作来测试参数的主要内容,如果未能解决你的问题,请参考以下文章