Search code examples
typescriptreduxjestjsredux-async-actions

Testing redux async actions with Typescript


Hey guys I am trying to test my async actions creators with Typescript but I am getting a type error that I cannot manage to solve.

This is my actions:

export const loadCurrentUserRequest = () => ({
  type: LOAD_CURRENT_USER_REQUEST
})

export const loadCurrentUserSuccess = (payload: any) => ({
  type: LOAD_CURRENT_USER_SUCCESS,
  payload
})

export const loadCurrentUserFailure = (payload: any) => ({
  type: LOAD_CURRENT_USER_FAILURE,
  payload
})

And this is my async action creator:

export const loadCurrentUser = () => {
  return async (dispatch: Dispatch<any>) => {
    dispatch(loadCurrentUserRequest())
    try {
      const response = await get(`api/currentuser`)
      if (!response.ok) {
        dispatch(loadCurrentUserFailure({ type: null, message: 'error' }))
      } else {
        const json = await response.json()
        dispatch(loadCurrentUserSuccess(json))
      }
      return response
    } catch (err) {
      dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }))
      logError(err.name, err.message)
      return err
    }
  }
}

The 'get' function is a middleware I've created to handle 'fetch' GET call (it adds some stuff into the header etc).

This is my test:

  it('create an action to load the current user', () => {
    const middlewares = [thunk]
    const mockStore = configureMockStore(middlewares)
    const store = mockStore()
    const expectActions = [{ type: LOAD_CURRENT_USER_REQUEST }]

    store.dispatch(actions.loadCurrentUser())
    expect(store.getActions()).toEqual(expectActions)
  })

I am getting this error in my console:

Argument of type '(dispatch: Dispatch<any>) => Promise<any>' is not assignable to parameter of type 'AnyAction'.
  Property 'type' is missing in type '(dispatch: Dispatch<any>) => Promise<any>' but required in type 'AnyAction'.

I am not sure what I have done wrong here, I looked at the redux example way to test async action creators and this is similar. I can't figure where my issue is coming from.

I do know that I will have to mock my fetch API calls and update my expectActions variable but the code isn't compiling because of that..


Solution

  • Here is the solution:

    index.ts:

    import fetch from 'node-fetch';
    import { ThunkDispatch } from 'redux-thunk';
    import { AnyAction } from 'redux';
    
    const logError = (name, message) => console.error(`name: ${name}, message: ${message}`);
    const get = url => fetch(url);
    
    export const LOAD_CURRENT_USER_REQUEST = 'LOAD_CURRENT_USER_REQUEST';
    export const LOAD_CURRENT_USER_SUCCESS = 'LOAD_CURRENT_USER_SUCCESS';
    export const LOAD_CURRENT_USER_FAILURE = 'LOAD_CURRENT_USER_FAILURE';
    
    export const loadCurrentUserRequest = () => ({
      type: LOAD_CURRENT_USER_REQUEST
    });
    
    export const loadCurrentUserSuccess = (payload: any) => ({
      type: LOAD_CURRENT_USER_SUCCESS,
      payload
    });
    
    export const loadCurrentUserFailure = (payload: any) => ({
      type: LOAD_CURRENT_USER_FAILURE,
      payload
    });
    
    export const loadCurrentUser = () => {
      return async (dispatch: ThunkDispatch<any, any, AnyAction>) => {
        dispatch(loadCurrentUserRequest());
        try {
          const response = await get(`api/currentuser`);
          if (!response.ok) {
            dispatch(loadCurrentUserFailure({ type: null, message: 'error' }));
          } else {
            const json = await response.json();
            dispatch(loadCurrentUserSuccess(json));
          }
          return response;
        } catch (err) {
          dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }));
          logError(err.name, err.message);
          return err;
        }
      };
    };
    
    

    Unit test:

    import configureStore from 'redux-mock-store';
    import thunk, { ThunkDispatch } from 'redux-thunk';
    import * as actions from './';
    import { AnyAction } from 'redux';
    import fetch from 'node-fetch';
    
    jest.mock('node-fetch', () => jest.fn());
    
    const initialState = {};
    type State = typeof initialState;
    const middlewares = [thunk];
    const mockStore = configureStore<State, ThunkDispatch<State, any, AnyAction>>(middlewares);
    const store = mockStore(initialState);
    
    describe('action creators', () => {
      describe('#loadCurrentUser', () => {
        afterEach(() => {
          store.clearActions();
        });
        it('load current user success', async () => {
          const userMocked = { userId: 1 };
          (fetch as jest.MockedFunction<any>).mockResolvedValueOnce({
            ok: true,
            json: jest.fn().mockResolvedValueOnce(userMocked)
          });
          await store.dispatch(actions.loadCurrentUser());
          expect(fetch).toBeCalledWith('api/currentuser');
          expect(store.getActions()).toEqual([
            { type: actions.LOAD_CURRENT_USER_REQUEST },
            { type: actions.LOAD_CURRENT_USER_SUCCESS, payload: userMocked }
          ]);
        });
    
        it('load current user failed', async () => {
          (fetch as jest.MockedFunction<any>).mockResolvedValueOnce({ ok: false });
          await store.dispatch(actions.loadCurrentUser());
          expect(fetch).toBeCalledWith('api/currentuser');
          expect(store.getActions()).toEqual([
            { type: actions.LOAD_CURRENT_USER_REQUEST },
            {
              type: actions.LOAD_CURRENT_USER_FAILURE,
              payload: {
                type: null,
                message: 'error'
              }
            }
          ]);
        });
    
        it('load current user failed when fetch error', async () => {
          (fetch as jest.MockedFunction<any>).mockRejectedValueOnce(new Error('fetch error'));
          await store.dispatch(actions.loadCurrentUser());
          expect(fetch).toBeCalledWith('api/currentuser');
          expect(store.getActions()).toEqual([
            { type: actions.LOAD_CURRENT_USER_REQUEST },
            {
              type: actions.LOAD_CURRENT_USER_FAILURE,
              payload: {
                type: 'Error',
                message: 'fetch error'
              }
            }
          ]);
        });
      });
    });
    
    

    Unit test result with 100% coverage:

     PASS  src/stackoverflow/54693637/index.spec.ts
      action creators
        #loadCurrentUser
          ✓ load current user success (8ms)
          ✓ load current user failed (1ms)
          ✓ load current user failed when fetch error (8ms)
    
      console.error src/stackoverflow/54693637/index.ts:3541
        name: Error, message: fetch error
    
    ----------|----------|----------|----------|----------|-------------------|
    File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    ----------|----------|----------|----------|----------|-------------------|
    All files |      100 |      100 |      100 |      100 |                   |
     index.ts |      100 |      100 |      100 |      100 |                   |
    ----------|----------|----------|----------|----------|-------------------|
    Test Suites: 1 passed, 1 total
    Tests:       3 passed, 3 total
    Snapshots:   0 total
    Time:        3.32s, estimated 5s
    

    Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/54693637