How to test that dispatch only gets called once

Viewed 97

I have a custom hook that dispatches an action when a URL parameter changes:

export const useUser = (): void => {
  const dispatch = useDispatch();

  const { user } = useParams<{ user: string }>();

  useEffect(() => {dispatch(getUser(user));
  }, [dispatch, user]);
};

I have a test that mocks useDispatch and useParams:

const dispatch = jest.fn();

const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch').mockImplementation(() => {
  return dispatch;
});

const useParamsSpy = jest.spyOn(reactRouter, 'useParams').mockImplementation(() => {
  return { user: 'test' };
});

const getUserSpy = jest.spyOn(slice, 'getUser');

renderHook(() => useRequestProfile());

expect(getUserSpy.mock.calls).toEqual([['test']]);

Now, this works, in that I can determine that getUser('test') has been called, but the point of wrapping it in useEffect is to make sure that it only get's called once. How can I test this?

2 Answers

You can put the useUser hook inside a component. Pass some props to this component and check. Later, pass the new props to re-render the component and check again if getUserSpy was called more than once.

Besides, we use MemoryRouter with initial entries instead of mocking useParams to provide URL parameters.

The expected result of our test is: regardless of how many times the component rendering, due to dispatch and user parameter are stable, the useEffect in the useUser hook will only be executed once.

E.g.

useUser.ts:

import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import * as slice from './slice';

export const useUser = (): void => {
  const dispatch = useDispatch();

  const { user } = useParams<{ user: string }>();
  useEffect(() => {
    dispatch(slice.getUser(user));
  }, [dispatch, user]);
};

slice.ts:

export const getUser = (user) => ({ type: 'GET_USER', payload: user });

useUser.test.tsx:

import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter, Route } from 'react-router-dom';
import { createStore } from 'redux';
import * as slice from './slice';
import { useUser } from './useUser';

describe('useUser', () => {
  test('should pass', () => {
    const getUserSpy = jest.spyOn(slice, 'getUser');
    function TestComp(props) {
      console.log(props);
      useUser();
      return null;
    }
    const store = createStore(() => ({}));

    const { rerender } = renderHook(TestComp, {
      initialProps: { data: '1' },
      wrapper: ({ children }) => (
        <Provider store={store}>
          <MemoryRouter initialEntries={['/test']}>
            <Route path="/:user">{children}</Route>
          </MemoryRouter>
        </Provider>
      ),
    });
    expect(getUserSpy).toBeCalledWith('test');
    rerender({ data: '2' });
    expect(getUserSpy).toBeCalledTimes(1);
  });
});

Test result:

 PASS  stackoverflow/70928091/useUser.test.tsx (7.748 s)
  useUser
    ✓ should pass (39 ms)

  console.log
    { data: '1' }

      at TestComp (stackoverflow/70928091/useUser.test.tsx:14:15)

  console.log
    { data: '2' }

      at TestComp (stackoverflow/70928091/useUser.test.tsx:14:15)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |     100 |     100 |                   
 slice.ts   |     100 |      100 |     100 |     100 |                   
 useUser.ts |     100 |      100 |     100 |     100 |                   
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.253 s, estimated 9 s
Related