Jest - Test catch block in an asyncThunk (Redux toolkit) - javascript

I've been struggling recently because I'm trying to fix some tests that another developer made before he left the company I'm working on. It involves testing a catch block inside a createAsyncThunk, the thunk was created as follows:
export const onEmailSubmit = createAsyncThunk(
'email/onEmailSubmit',
async (data, { dispatch, rejectWithValue, getState }) => {
dispatch(updateEmail({ isLoadingEmailRequest: true }))
try {
const response = await updateIdentity(data)
return response.data
} catch (err) {
// If the error doesn't have any status code, there is a Network Error
// so let's show the error modal:
if (err.request) {
dispatch(updateErrorModal({ showErrorModal: true }))
}
return rejectWithValue(err.data)
}
})
The test file is something along the lines:
import userReducer, { updateEmail, handleEmailVerify } from './EmailSlice'
import configureStore from 'redux-mock-store' // ES6 modules
import thunk from 'redux-thunk'
import { updateIdentity } from '../../../http'
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
jest.mock('../../../http/', () => ({
...jest.requireActual('../../../http/'),
updateIdentity: jest.fn().mockImplementation((data) => {
return Promise.resolve({
simpleFieldsValues: {
displayName: 'test',
language: 'en_US'
}
})
})
}))
describe('EmailSlice unit tests', () => {
beforeEach(() => {
jest.clearAllMocks()
Object.defineProperty(window, 'sessionStorage', {
value: {
getItem: jest.fn(() => null),
setItem: jest.fn(() => null),
removeItem: jest.fn(() => null)
},
writable: true
})
})
afterEach(() => {
jest.restoreAllMocks()
})
// ...Some tests before this one
it('tests when handleEmailVerify is called and backend does not return a response', async () => {
const error = new Error()
error.request = {
message: 'test'
}
sendVerificationCode.mockImplementation(_ => Promise.reject(error))
const store = mockStore({})
await store.dispatch(handleEmailVerify())
const actions = store.getActions()
const rejectedActionLoading = actions[1]
expect(rejectedActionLoading.type).toEqual(updateEmail.type)
expect(rejectedActionLoading.payload).toEqual({ isLoadingEmailRequest: true })
const rejectedAction = actions[2]
expect(rejectedAction.type).toEqual("errorModal/updateErrorModal")
})
})
The thing is that the tests fail because the dispatch to update the error modal is never reached. I think this is related to the fact that the mocked promise sendVerificationCode.mockImplementation(_ => Promise.reject(error)) does not return the object with the request object inside of it to the catch block, thus not dispatching the updateErrorModal.
PS: If I remove the if statement and just dispatch the updateErrorModal, the test passes.
Do you guys have any idea how to fix this?
Thanks for your time :)

Related

get new Instance for function each test

I have some tests in one file,
I check my reducer with some case
My code looks like this
my code
import axiosInstance from '~/utils/network';
const fetcher = axiosInstance();
const fetchMiddleware = () => {
switch (type) {
case 'LOGOUT':{
try {
await fetcher.get(API.GET.LOGOUT_OPERATION);
dispatch({ type: 'LOGOUT_SUCCESS' });
} catch (err) {
dispatch({ type: 'LOGOUT_FAIL' });
}
});
}
}
}
my test
import axiosInstance from '../../src/utils/network';
import configureStore from 'redux-mock-store';
const middlewares = [fetchMiddleware, thunk];
const mockStore = configureStore(middlewares);
const store = mockStore(getInitialReducerState());
jest.mock('../../src/utils/network', () => {
const axiosInstance = jest.fn().mockImplementation(() => {
return {
get: jest.fn().mockImplementation(() => {
return {
headers: {},
};
}),
};
}) as any;
axiosInstance.configure = jest.fn();
return axiosInstance;
});
describe('test LOGOUT', () => {
beforeEach(() => {
store.clearActions();
});
it('should test be success', async () => {
await store.dispatch({
type: 'LOGOUT',
payload: { userName: 'testUserName' },
});
expect(store.getActions()).toContainEqual({
type: 'LOGOUT_SUCCESS',
});
});
it('should test be fail', async () => {
(axiosInstance as jest.Mock).mockImplementation(() => {
return {
get: jest.fn().mockImplementation(() => {
throw new Error(' ');
}),
};
});
await store.dispatch({
type: 'LOGOUT',
payload: { userName: 'testUserName' },
});
expect(store.getActions()).toContainEqual({
type: 'LOGOUT_FAIL',
});
});
});
I want to test two scenarios: success & fail,
I mock the axiosInstance function.
But even I override the mock in the second test I get the first mock because my code loads axiosInstance only once.
what can I do?
You need to use jest.isolateModules
Let's say we have 2 files:
./lib.js - this is your ~/utils/network
./repro.js - this is your file with the code under test
./lib.js:
export default function lib() {
return () => 10;
}
./repro.js:
import lib from './lib';
const fnInstance = lib();
export const fn = () => {
return fnInstance();
};
And the ./repro.test.js:
function getRepro(libMock) {
let repro;
// Must use isolateModules because we need to require a new module everytime
jest.isolateModules(() => {
jest.mock('./lib', () => {
return {
default: libMock,
};
});
repro = require('./repro');
});
// If for some reason in the future the behavior will change and this assertion will fail
// We can do a workaround by returning a Promise and the `resolve` callback will be called with the Component in the `isolateModules` function
// Or we can also put the whole test function inside the `isolateModules` (less preferred)
expect(repro).toBeDefined();
return repro;
}
describe('', () => {
it('should return 1', () => {
const { fn } = getRepro(function lib() {
return () => 1
});
expect(fn()).toEqual(1);
});
it('should return 2', () => {
const { fn } = getRepro(function lib() {
return () => 2
});
expect(fn()).toEqual(2);
});
});
It's preferable to use existing library to mock Axios, it saves from boilerplate code and potential mistakes in mock implementation; moxios has been already suggested.
It's inconvenient to mock axiosInstance per test because it has been already called on the import of tested module, so this requires it to be re-imported per test; another answer explains how it's done with jest.isolateModules.
Since axiosInstance is evaluated only once and is supposed to return mocked object, it's convenient to mock it once per test and then change implementations:
jest.mock('~/utils/network', () => {
const axiosMock = { get: jest.fn(), ... };
return {
axiosInstance: () => axiosMock;
};
});
const axiosMock = axiosInstance();
...
(axiosMock.get axiosInstance as jest.Mock).mockImplementation(() => {
throw new Error(' ');
});
await store.dispatch(...);
This requires to use jest.restoreAllMocks in beforeEach or similar Jest configuration option to avoid test cross-contamination.
Notice that Axios doesn't throw errors but rather return rejected promises, this may affect test results, see the note regarding the benefits of libraries.

I'm writing a test case in React related to authentication(using axios), But I am stuck because the test case is not passing

Existing code:
loginUser.js:
import { getUserDetails } from '../api/userDetails';
import { mapApiObjectToModel } from '../mapper/userProfileMapper';
import axios from 'axios';
export const getLoggedInUserDetails = async () => {
axios
.get('/api/getUserDetails')
.then(response => {
return mapApiObjectToModel(response);
})
.catch(err => {
console.log('error==', err);
});
};
userProfileMapper.js:
export const mapApiObjectToModel = inputObj => {
const outputObj = {};
const authorizedRoles = ['Admin'];
if (inputObj) {
outputObj.fullName = '';
if (inputObj.data) {
outputObj.fullName = inputObj.data.data;
}
outputObj.role = 'Admin';
outputObj.isAuthorized = authorizedRoles.includes(outputObj.role);
}
console.log('outputObj', outputObj);
return outputObj;
};
loginUser.test.js:
import axios from 'axios';
import getLoggedInUserDetails from '../../action/loginUser';
jest.mock('axios');
describe('routes using memory router', () => {
it('Get Admin message', async () => {
const data = 'Admin';
axios.get.mockImplementationOnce(() => Promise.resolve(data));
console.log(data);
await expect(getLoggedInUserDetails()).resolves.toEqual(data);
expect(axios.get).toHaveBeenCalledWith('/api/getUserDetails');
});
it('fetches erroneously data from an API', async () => {
const errorMessage = 'Network Error';
axios.get.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)));
await expect(getLoggedInUserDetails()).rejects.toThrow(errorMessage);
});
});
I'm really new to all these, so any assistance would be appreciated. Even any suggestions on TDD for userProfileMapper.js would be appreciated :)
The mapApiObjectToModel returns an object like,
{
role: 'ADMIN',
isAuthorized: true
}
However, in your test, you are expecting it to be equal to a string 'Admin'
const data = 'Admin';
...
await expect(getLoggedInUserDetails()).resolves.toEqual(data); // Note that you have initialize data to 'Admin'
Try changing data to be an object, like,
const data = {
role: 'Admin',
isAuthorized: true
};
...
await expect(getLoggedInUserDetails()).resolves.toEqual(data);
Updated: loginUser.js:
import { getUserDetails } from '../api/userDetails';
import { mapApiObjectToModel } from '../mapper/userProfileMapper';
import axios from 'axios';
export const getLoggedInUserDetails = () => {
return axios
.get('/api/getUserDetails')
.then(response => {
return mapApiObjectToModel(response);
})
.catch(err => {
console.log('error==', err);
throw err;
});
};
You function getLoggedInUserDetails had following issues,
You were not returning the promise from the function.
You don't need async here as you are accessing expect(Promise).resolves in your test file loginUser.test.js:.
You need to throw err from catch block, if you want to test the rejection of promise or remove the catch block.
I have updated following items to the function getLoggedInUserDetails,
removed the async from export const getLoggedInUserDetails = async () => {
returned promise from axios.get('/api/getUserDetails')
added throw err to catch block
You should not mix usage of Promise.then and async/await for more information on the difference between them check here and here

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?
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.
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: []
});
});
});

JS/JestJS: How to test for a async mocked function?

As you can see in my testfile I've mocked all mongoDB methods.
Now I need to test, if Content.update() has been called. In this example code I'm testing for a called console.log which is working, but not want I want.
I don't understand why I can't test for update()
/category.test.js
import { updateCategory } from './category'
import DB from './lib/db'
test('should update document', async () => {
DB.getDB = jest.fn(
() => ({
get: jest.fn(
() => ({
findOne: jest.fn(() => ({ some: 'content' })),
update: jest.fn()
})
)
})
)
console.log = jest.fn()
return updateCategory({}, {
id: '12345678901234567'
}).then(() => {
expect(console.log).toHaveBeenCalled()
// instead check if `Content.update()` has been called
expect(DB.getDB().get().update).toHaveBeenCalled() // throws `Expected mock function to have been called.`
})
})
/category.js
import DB from './lib/db'
export async function updateCategory (obj, { id }) {
const db = DB.getDB()
const Content = db.get('content')
const doc = await Content.findOne({ _id: id })
console.log('ok');
await Content.update(
{ _id: id },
{ $set: { category: 'new category' } }
)
}
Store the mock you want to spy on in a variable so you can track it:
import { updateCategory } from './category'
import DB from './lib/db'
jest.mock('./lib/db');
const mockUpdate = jest.fn();
test('should update document', async () => {
DB.getDB.mockImplementation( // <-- available when you call jest.mock above, seems safer than overwriting the implementation in the real import
() => ({
get: jest.fn(
() => ({
findOne: jest.fn(() => ({ some: 'content' })),
update: mockUpdate
})
)
})
)
console.log = jest.fn()
return updateCategory({}, {
id: '12345678901234567'
}).then(() => {
expect(console.log).toHaveBeenCalled()
// instead check if `Content.update()` has been called
expect(mockUpdate).toHaveBeenCalled() // should work now
})
})

Jest/enzyme: How to test for .then and .catch of an nested asynchronous function

I'm trying to do some enzyme/jest unit testing for a asynchronous function in my reactJS component, which gets injected as prop.
My Problem is to test for a value in the then() part of the function and to test for catch() if an error occures.
This is how the function of my component (<CreateAccount />) looks like:
_onSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
this.props.createUserMutation({
variables: { username, password }
}).then(response => {
const token = response.data.createUser.token
if (token) {
Cookies.set('auth-token', token, { expires: 1 })
}
}).catch(error => {
console.warn(error)
})
}
The first test should check for .catch(error => {}) as data is undefined:
it('_onSubmit() should throw error if data is missing', () => {
const createUserMutation = () => {
return Promise.resolve({})
}
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
})
const state = wrapper.instance().state
expect(wrapper).toThrow() // <-- How to do it correctly?
})
And the second test should check if cookie is set correctly. Here I don't know how to do that? I think I have to mock Cookie
it('_onSubmit() should get token', () => {
const createUserMutation = () => {
return Promise.resolve({
data: {
createUser: { token: 'token' }
}
})
}
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.find(Form).simulate('submit', {
preventDefault: () => {}
})
// How to check for token value and Cookies?
})
What I usually have to do when I want to see if the spy worked on the catch or then, is to add another then() on the test. For example:
it('_onSubmit() should throw error if data is missing', () => {
const createUserMutation = jest.fn(() => Promise.reject(new Error()));
const spy = jest.spyOn(console,"warn");
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
});
return createUserMutation.catch(() => {
expect(wrapper).toMatchSnapshot();
})
.then(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
})
I guess it is somehow related to how NodeJS handles the queues, promises, ticks, etc, internally.
That is the rejected/catch branch. If you want to test the IF path, just use a Promise.resolve and return a promise.then() instead of catch.
Why are you using console.warn for an error? Use console.error instead. You will need to mock it out to a spy as well to test it.
First test:
it('_onSubmit() should throw error if data is missing', (done) => {
const createUserMutation = () => new Promise();
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
})
const state = wrapper.instance().state
createUserMutation.resolve().then(() => {
expect(console.warn).toHaveBeenCalled();
done();
});
})
If you are running this in a mock browser environment and not a real browser then you must mock out Cookies.set.
Second test:
it('_onSubmit() should get token', (done) => {
const createUserMutation = () => new Promise();
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.find(Form).simulate('submit', {
preventDefault: () => {}
});
jest.spyOn(window.Cookies, 'set');
const response = {
data: {
createUser: { token: 'token' }
}
}
createUserMutation.resolve(response).then(() => {
expect(window.Cookies.set).toHaveBeenCalled();
done();
});
})
afterEach(() => {
// Reset the spies so that they don't leak to other tests
jest.restoreAllMocks();
});

Categories