mock useDispatch in jest and test the params with using that dispatch action in functional component

45,154

Solution 1

[upd] I've changed my mind dramatically since then. Now I think mocking store(with redux-mock-store or even real store that changes its state) - and wrapping component with <Provider store={mockedStore}> - is way more reliable and convenient. Check another answer below.

if you mock react-redux you will be able to verify arguments for useDispatch call. Also in such a case you will need to re-create useSelector's logic(that's really straightforward and actually you don't have to make mock be a hook). Also with that approach you don't need mocked store or <Provider> at all.

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*/);
});

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

From functional component we mock dispatch like above to stop it to execute the real implementation. Hope it helps!

Solution 3

This is how I solved using react testing library:

I have this wrapper to render the components with 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;
}

Using this I can call store.dispatch from inside the test and check if it was called with the action I want.

  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"));
  });

Solution 4

I see advantages in using actual <Provider store={store}>:

  • much easier to write tests
  • much more readable since just store's data is actually mocked(one mock instead of multiple - and sometimes inconsistent - mocks for useDispatch and useSelector)

But introducing real store with real reducer(s) and real dispatching looks like overkill to me as for unit testing(but would be ok to integration testing):

  • mocking all the server requests might be a huge task
  • typically we already have that logic covered with test on per-slice basis

With this in mind have picked configureStore from redux-mock-store instead redux and got next helper(uses Enzyme):

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

And example of usage:

    const {
      getComponent,
      replaceStore,
    } = renderInRedux(<Loader />, { isRequesting: false });

    expect(getComponent().isEmptyRender()).toBeTruthy();

    replaceStore({ isRequesting: true });
    expect(getComponent().isEmptyRender()).toBeFalsy();

But how would it help to avoid mocking server side interaction if we want to test dispatching? Well, by itself it does not. But we can mock and test action dispatching in easy way:

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

Solution 5

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

UPDATE: DO NOT use React Redux hooks API which is strongly coupling with Redux store implementation logic, make it very difficult to test.

Share:
45,154
Shubham Singhal
Author by

Shubham Singhal

Updated on July 18, 2022

Comments

  • Shubham Singhal
    Shubham Singhal almost 2 years

    Hi I am writing test for functional component using the jest and enzyme. and When I simulate a click then params(state of component using useState) of component change. and when state is changed then useEffect call and in useEffect I am dispatching some asynchronous actions with params after changed. So I want to test params with I am dispatching the action. for this I want to mock dispatch. How can I achieve this ? Anyone can help me, thanks in advance. Below I am sharing the code.

    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')
    
    
    
      })
    
    });