Leon Gaban
Leon Gaban

Reputation: 39018

How to test catch statement in async await Action

Problem

I have an Action which awaits an API function. The happy path in the try is easily testable with my mocked API. However, unsure as to the best way to test and cover the .catch.

Actions

import {getRoles} from '../shared/services/api';

export const Actions = {
    SET_ROLES: 'SET_ROLES'
};

export const fetchRoles = () => async dispatch => {
    try {
        const response = await getRoles();
        const roles = response.data;

        dispatch({
            type: Actions.SET_ROLES,
            roles
        });
    } catch (error) {
        dispatch({
            type: Actions.SET_ROLES,
            roles: []
        });
    }
};

Actions Test

import {fetchRoles} from '../party-actions';
import rolesJson from '../../shared/services/__mocks__/roles.json';
jest.mock('../../shared/services/api');

describe('Roles Actions', () => {
    it('should set roles when getRoles() res returns', async () => {
        const mockDispatch = jest.fn();

        await fetchRoles()(mockDispatch);
        try {
            expect(mockDispatch).toHaveBeenCalledWith({
                type: 'SET_ROLES',
                roles: rolesJson
            });
        } catch (e) {
            // console.log('fetchRoles error: ', e)
        }
    });

    // Here is the problem test, how do we intentionally cause
    // getRoles() inside of fetchRoles() to throw an error?
    it('should return empty roles if error', async () => {
        const mockDispatch = jest.fn();

        await fetchRoles('throwError')(mockDispatch);

        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'SET_ROLES',
            roles: []
        });
    });
});

Mocked API

import rolesJson from './roles.json';

export const getRoles = async test => {
    let mockGetRoles;

    if (test === 'throwError') {
        //   console.log('sad')
        mockGetRoles = () => {
            return Promise.reject({
                roles: []
            });
        };
    } else {
        // console.log('happy')
        mockGetRoles = () => {
            return Promise.resolve({
                roles: rolesJson
            });
        };
    }

    try {
        const roles = mockGetRoles();
        // console.log('api mocks roles', roles);
        return roles;
    } catch (err) {
        return 'the error';
    }
};

^ Above you can see what I tried, which did work, but it required me to change my code in a way that fit the test, but not the actual logic of the app.

For instance, for this test to pass, I have to pass in a variable through the real code (see x):

export const fetchRoles = (x) => async dispatch => {
    try {
        const response = await getRoles(x);
        const roles = response.data;

How can we force getRoles in our mock to throw an error in our sad path, .catch test?

Upvotes: 3

Views: 954

Answers (2)

Leon Gaban
Leon Gaban

Reputation: 39018

I resolved the test and got the line coverage for the .catch by adding a function called mockGetRolesError in the mock api file:

Thanks to @skyboyer for the idea to have a method on the mocked file.

import {getRoles} from '../shared/services/api';

export const Actions = {
    SET_ROLES: 'SET_ROLES'
};

export const fetchRoles = () => async dispatch => {
    try {
        const response = await getRoles();
        const roles = response.data;
        // console.log('ACTION roles:', roles);

        dispatch({
            type: Actions.SET_ROLES,
            roles
        });
    } catch (error) {
        dispatch({
            type: Actions.SET_ROLES,
            roles: []
        });
    }
};

Now in the test for the sad path, I just have to call mockGetRolesError to set the internal state of the mocked api to be in a return error mode.

import {fetchRoles} from '../party-actions';
import rolesJson from '../../shared/services/__mocks__/roles.json';
import {mockGetRolesError} from '../../shared/services/api';
jest.mock('../../shared/services/api');

describe('Roles Actions', () => {
    it('should set roles when getRoles() res returns', async () => {
        const mockDispatch = jest.fn();

        try {
            await fetchRoles()(mockDispatch);
            expect(mockDispatch).toHaveBeenCalledWith({
                type: 'SET_ROLES',
                roles: rolesJson
            });
        } catch (e) {
            return e;
        }
    });

    it('should return empty roles if error', async () => {
        const mockDispatch = jest.fn();

        mockGetRolesError();
        await fetchRoles()(mockDispatch);

        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'SET_ROLES',
            roles: []
        });
    });
});

Upvotes: 0

skyboyer
skyboyer

Reputation: 23705

You can mock getRoles API on per-test basis instead:

// getRoles will be just jest.fn() stub
import {getRoles} from '../../shared/services/api'; 
import rolesJson from '../../shared/services/__mocks__/roles.json';

// without __mocks__/api.js it will mock each exported function as jest.fn();
jest.mock('../../shared/services/api'); 

it('sets something if loaded successfully', async ()=> {
  getRoles.mockReturnValue(Promise.resolve(rolesJson));
  dispatch(fetchRoles());
  await Promise.resolve(); // so mocked API Promise could resolve
  expect(someSelector(store)).toEqual(...);
});

it('sets something else on error', async () => {
  getRoles.mockReturnValue(Promise.reject(someErrorObject));
  dispatch(fetchRoles());
  await Promise.resolve();
  expect(someSelector(store)).toEqual(someErrornessState);
})

I also propose you concentrate on store state after a call not a list of actions dispatched. Why? Because actually we don't care what actions in what order has been dispatched while we get store with data expected, right?

But sure, you still could assert against dispatch calls. The main point: don't mock result returned in __mocks__ automocks but do that on peer-basis.

Upvotes: 1

Related Questions