开玩笑地模拟 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 甚至改变其状态的真实存储) - 并用&lt;Provider store=mockedStore&gt; 包装组件 - 更加可靠和方便。检查下面的另一个答案。

如果您模拟react-redux,您将能够验证useDispatch 调用的参数。同样在这种情况下,您将需要重新创建useSelector 的逻辑(这真的很简单,实际上您不必让 mock 成为一个钩子)。同样使用这种方法,您根本不需要模拟商店或&lt;Provider&gt;

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(() =&gt; store.dispatch) useSelector.mockImplementation(() =&gt; ); it('loads data on init', () =&gt; const historyMock = push: jest.fn() ; let wrapper; wrapper = mount(&lt;Router&gt;&lt;Clients history=historyMock /&gt;&lt;/Router&gt;); 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】:

我看到了使用实际&lt;Provider store=store&gt; 的优势:

更容易编写测试 可读性更强,因为实际上只模拟了 store 的数据(一个模拟而不是多个 - 有时不一致 - 模拟 useDispatchuseSelector

但是对于单元测试来说,引入真正的 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 并在功能组件中使用该调度操作来测试参数的主要内容,如果未能解决你的问题,请参考以下文章

如何开玩笑地模拟 VueAxios

在玩笑中模拟功能组件会引发“无效的变量访问”错误

模拟 React useRef 或带有酶和玩笑的功能组件内的函数?

开玩笑的预期模拟未调用(redux 组件)

开玩笑地从模拟文件中窥探模拟函数

开玩笑地模拟“this”对象上的方法