redux-thunk structure and test side effects - javascript

I am using redux-thunk and not sure if side effects (showAlertError function) are structured properly. Although my jest test setup seems to be fine at first glance, I get an error:
jest.fn() value must be a mock function or spy. Received: undefined`
Is the showAlertError function is at the right place or it should be in the action creator or somewhere else? Also if this is the right place for it then how I can test if it's called.
export const submitTeammateInvitation = (data) => {
const config = {
// config code
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest(data));
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess(jsonResponse));
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { showAlertError } from '../../../../_helpers/alerts';
jest.mock('../../../../_helpers/alerts');
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
describe('submitTeammateInvitation', () => {
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
submitTeammateInvitationRequestObject,
submitTeammateInvitationErrorObject
];
const showAlertError = jest.fn();
return store.dispatch(submitTeammateInvitation(inviteTeammateEndpoint))
.then(() => {
expect(showAlertError).toBeCalled(); // this doesn't work
expect(store.getActions()).toEqual(expectedActions); // this works
});
});
});

You can mock showErrorAlert function manually. Here is the solution:
actionCreators.ts:
import fetch from 'node-fetch';
import { showErrorAlert } from './showErrorAlert';
const SUBMIT_TEAMATE_INVITATION_REQUEST = 'SUBMIT_TEAMATE_INVITATION_REQUEST';
const SUBMIT_TEAMATE_INVITATION_SUCCESS = 'SUBMIT_TEAMATE_INVITATION_SUCCESS';
const SUBMIT_TEAMATE_INVITATION_ERROR = 'SUBMIT_TEAMATE_INVITATION_ERROR';
export const submitTeammateInvitationRequest = data => ({ type: SUBMIT_TEAMATE_INVITATION_REQUEST, payload: { data } });
export const submitTeammateInvitationSuccess = data => ({ type: SUBMIT_TEAMATE_INVITATION_SUCCESS, payload: { data } });
export const submitTeammateInvitationError = data => ({ type: SUBMIT_TEAMATE_INVITATION_ERROR, payload: { data } });
export const submitTeammateInvitation = data => {
const config = {
// config code
};
const inviteTeammateEndpoint = 'https://github.com/mrdulin';
return async dispatch => {
dispatch(submitTeammateInvitationRequest(data));
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess(jsonResponse));
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
showErrorAlert.ts:
export function showErrorAlert(jsonResponse) {
console.log(jsonResponse);
}
actionCreators.spec.ts:
import {
submitTeammateInvitation,
submitTeammateInvitationRequest,
submitTeammateInvitationSuccess,
submitTeammateInvitationError
} from './actionCreators';
import createMockStore from 'redux-mock-store';
import thunk, { ThunkDispatch } from 'redux-thunk';
import fetch from 'node-fetch';
import { AnyAction } from 'redux';
import { showErrorAlert } from './showErrorAlert';
const { Response } = jest.requireActual('node-fetch');
jest.mock('node-fetch');
jest.mock('./showErrorAlert.ts', () => {
return {
showErrorAlert: jest.fn()
};
});
const middlewares = [thunk];
const mockStore = createMockStore<any, ThunkDispatch<any, any, AnyAction>>(middlewares);
describe('submitTeammateInvitation', () => {
it('dispatches the correct actions on a failed fetch request', () => {
const mockedResponse = { data: 'mocked response' };
const mockedJSONResponse = JSON.stringify(mockedResponse);
const mockedData = { data: 'mocked data' };
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce(
new Response(mockedJSONResponse, { status: 500, statusText: 'Internal Server Error' })
);
const intialState = {};
const store = mockStore(intialState);
const expectedActions = [
submitTeammateInvitationRequest(mockedData),
submitTeammateInvitationError(mockedResponse)
];
return store.dispatch(submitTeammateInvitation(mockedData)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(showErrorAlert).toBeCalledWith(mockedResponse);
});
});
});
Unit test result with coverage report:
PASS src/stackoverflow/47560126/actionCreators.spec.ts
submitTeammateInvitation
✓ dispatches the correct actions on a failed fetch request (11ms)
-------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files | 89.29 | 50 | 83.33 | 90.91 | |
actionCreators.ts | 89.29 | 50 | 83.33 | 90.91 | 32,35 |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.864s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/47560126

Related

redux test with jest

I mocked the "createTask" function in the "api":
import configureMockStore from "redux-mock-store";
import { createTask } from "../../actions/tasks";
import * as api from "../../api";
import thunk from "redux-thunk";
jest.mock('../../api')
// mock api module
api.createTask = jest.fn(() => {
return Promise.resolve({data : 'foo'})
})
// configuration mock store
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
// suite tests
describe("create task action create store<async>", () => {
test("works", () => {
const expectedActions = [
{ type: "REQUEST_STARTED" },
{
type: "CREATE_TASK_SUCCEED",
payLaod: { task: "foo" },
meta: { analytics: { event: "create_task", data: { id: undefined } } },
},
];
const store = mockStore({
tasks: {
tasks: [],
},
});
return store.dispatch(createTask({})).then(() => {
expect(store.getActions()).isEqual(expectedActions);
expect(api.createTask).toHaveBeenCalled();
});
});
});
But after running the test, I get this error. seem my mock function does not return a promise.
● create task action create store<async> › works
TypeError: Cannot read properties of undefined (reading 'then')
23 | return (dispatch) => {
24 | dispatch(requestStarted());
> 25 | api
| ^
26 | .createTask({ title, description, status, timer, projectId })
27 | .then((resp) => {
28 | dispatch(createTaskSucceed(resp.data));
I do not know the reason for this. Can anyone help me?
createTask action creator code
function createTask({
title,
description,
status = "Unstarted",
timer = 0,
projectId,
}) {
return (dispatch) => {
dispatch(requestStarted());
api
.createTask({ title, description, status, timer, projectId })
.then((resp) => {
dispatch(createTaskSucceed(resp.data));
})
.catch((error) => {
dispatch(requestFailed(error));
});
};
}
I also added action creator code to show what is happening in this function

Need help in React Saga Unit test

I am facing an isse while testing my saga function:
function * onSaveDATA() {
try {
yield put( showStatusMessage({ messageContent: 'Saving Your Data' }));
const body = yield select( state => state.appData.userDetails );
yield call( postDATA, { body });
yield put( hideStatusMessage());
yield put({ type: ActionTypes.SAVE_DATA_OK });
} catch ( e ) {
yield put({ type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: e });
}
}
export function * save_on_change( ) {
yield takeEvery( ActionTypes.SAVE_DATA_REQ, onSaveDATA );
}
Here is a unit test which I have written to test this function, but it is failing the test. I am not sure what is wrong.
import { runSaga } from 'redux-saga';
import { postDATA } from './../../../../services/my_service';
// import { openModalMessage } from './../../../../actions';
import { saveDATA } from './';
jest.mock( './../../../../services/my_service' );
jest.mock( './../../../../actions' );
describe( 'Saga: Save Data', () => {
test( 'saveDATA OK', async () => {
postDATA.mockReset();
postDATA.mockReturnValue( {s:'Somevalue'} );
const dispatchedActions = [];
await runSaga({
dispatch: action => dispatchedActions.push( action ),
getState: () => ({
appState: {},
appData: { userDetails: {name:'mock-name'}},
}),
}, save_on_change );
expect( postDATA ).toHaveBeenCalled();
});
});
When I run this it fails. I am not sure what am I missing here. Is it because the saveDATA function is using factory function takeEvery. Do I need to explicilty trigger the action SAVE_DATA_REQ?
Here is unit test solution for "redux-saga": "^1.1.3":
index.ts:
import { put, select, call, takeEvery } from 'redux-saga/effects';
import { postDATA } from './service';
export const ActionTypes = {
SAVE_DATA_OK: 'SAVE_DATA_OK',
CRITICAL_ERROR_OCCURED: 'CRITICAL_ERROR_OCCURED',
SAVE_DATA_REQ: 'SAVE_DATA_REQ',
};
const showStatusMessage = (payload) => ({ type: 'SHOW_STATUS_MESSAGE', payload });
const hideStatusMessage = () => ({ type: 'HIDE_STATUS_MESSAGE' });
export function* onSaveDATA() {
try {
yield put(showStatusMessage({ messageContent: 'Saving Your Data' }));
const body = yield select((state) => state.appData.userDetails);
yield call(postDATA, { body });
yield put(hideStatusMessage());
yield put({ type: ActionTypes.SAVE_DATA_OK });
} catch (e) {
yield put({ type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: e });
}
}
export function* save_on_change() {
yield takeEvery(ActionTypes.SAVE_DATA_REQ, onSaveDATA);
}
service.ts:
export async function postDATA(data) {
return { s: 'real data' };
}
index.test.ts:
import { runSaga } from 'redux-saga';
import { onSaveDATA, ActionTypes, save_on_change } from './';
import { postDATA } from './service';
import { mocked } from 'ts-jest/utils';
import { takeEvery } from 'redux-saga/effects';
jest.mock('./service');
describe('62952662', () => {
afterAll(() => {
jest.resetAllMocks();
});
describe('onSaveDATA', () => {
test('should save data', async () => {
mocked(postDATA).mockResolvedValueOnce({ s: 'Somevalue' });
const dispatchedActions: any[] = [];
await runSaga(
{
dispatch: (action) => dispatchedActions.push(action),
getState: () => ({
appState: {},
appData: { userDetails: { name: 'mock-name' } },
}),
},
onSaveDATA,
).toPromise();
expect(postDATA).toBeCalledWith({ body: { name: 'mock-name' } });
expect(dispatchedActions).toEqual([
{ type: 'SHOW_STATUS_MESSAGE', payload: { messageContent: 'Saving Your Data' } },
{ type: 'HIDE_STATUS_MESSAGE' },
{ type: ActionTypes.SAVE_DATA_OK },
]);
});
test('should handle error if postDATA error', async () => {
const mError = new Error('network');
mocked(postDATA).mockRejectedValueOnce(mError);
const dispatchedActions: any[] = [];
await runSaga(
{
dispatch: (action) => dispatchedActions.push(action),
getState: () => ({
appState: {},
appData: { userDetails: { name: 'mock-name' } },
}),
},
onSaveDATA,
).toPromise();
expect(postDATA).toBeCalledWith({ body: { name: 'mock-name' } });
expect(dispatchedActions).toEqual([
{ type: 'SHOW_STATUS_MESSAGE', payload: { messageContent: 'Saving Your Data' } },
{ type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: mError },
]);
});
});
describe('save_on_change', () => {
test('should wait for every SAVE_DATA_REQ action and call onSaveDATA', () => {
const gen = save_on_change();
expect(gen.next().value).toEqual(takeEvery(ActionTypes.SAVE_DATA_REQ, onSaveDATA));
expect(gen.next().done).toBeTruthy();
});
});
});
unit test results with coverage report:
PASS src/stackoverflow/62952662/index.test.ts
62952662
onSaveDATA
✓ should save data (6 ms)
✓ should handle error if postDATA error (2 ms)
save_on_change
✓ should wait for every SAVE_DATA_REQ action and call onSaveDATA (1 ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 95 | 100 | 83.33 | 93.75 |
index.ts | 100 | 100 | 100 | 100 |
service.ts | 50 | 100 | 0 | 50 | 2
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.928 s, estimated 3 s

How to test axios wrapper

I have no idea how to test axios wrapper functions. What should I mock?
I have createApiClient.js file:
import createApiClient from '../createApiClient';
import axios from 'axios';
function createApiClient(config = {}) {
const client = axios.create(config);
client.interceptors.response.use((response) => response.data,
(error) => {
if (error.response) {
throw error.response.data;
} else {
throw new Error('Ошибка во время соединения с сервером! Попробуйте повторить попытку позже.');
}
});
return client;
}
export default createApiClient;
Also I have concrete client.js file created with this function:
import createApiClient from '../createApiClient';
const request = createApiClient({
baseURL: process.env.VUE_APP_AUTH_API_URL,
});
async function logIn(username, password) {
const { token } = await request.post('login/', {
username,
password,
});
return token;
}
// other functions...
export { logIn, register, getUserInfo };
How to test logIn() and other functions in client.js? Especially, I'm wondering about axios.create(), interceptors, etc.
I tried this and some variations:
import createApiClient from '#/api/createApiClient';
import { logIn } from '#/api/auth/client';
const token = 'token';
describe('Тестирование API аутентификации', () => {
test('log in success', async () => {
const request = createApiClient();
request.post = jest.fn(() => Promise.resolve(token));
const response = await logIn('foo', 'qwerty');
response.toBe({ token });
});
});
You could use jest.mock(moduleName, factory, options) to mock ./createApiClient module, createApiClient function and its returned value.
E.g.
client.js:
import createApiClient from './createApiClient';
const request = createApiClient({
baseURL: process.env.VUE_APP_AUTH_API_URL,
});
async function logIn(username, password) {
const { token } = await request.post('login/', {
username,
password,
});
return token;
}
export { logIn };
createApiClient.js:
import axios from 'axios';
function createApiClient(config = {}) {
const client = axios.create(config);
client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
throw error.response.data;
} else {
throw new Error('Ошибка во время соединения с сервером! Попробуйте повторить попытку позже.');
}
},
);
return client;
}
export default createApiClient;
client.test.js:
import { logIn } from './client';
import createApiClientMock from './createApiClient';
jest.mock('./createApiClient', () => {
const axiosInstance = { post: jest.fn() };
return jest.fn(() => axiosInstance);
});
describe('61713112', () => {
it('should pass', async () => {
const axiosInstanceMock = createApiClientMock();
const mResponse = { token: 'token' };
axiosInstanceMock.post.mockResolvedValueOnce(mResponse);
const actual = await logIn('foo', 'qwerty');
expect(actual).toEqual('token');
expect(axiosInstanceMock.post).toBeCalledWith('login/', { username: 'foo', password: 'qwerty' });
});
});
unit test results with coverage report:
PASS stackoverflow/61713112/client.test.js
61713112
✓ should pass (4ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
client.js | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.582s, estimated 20s

Testing function with inner function that should be mocked

I have problem with testing this function. I don't know how I can test checkAuth when decode is mocked.
import decode from "jwt-decode";
export const checkAuth = () => {
const token = localStorage.getItem("token");
if (!token) {
return false;
}
try {
const { exp } = decode(token);
if (exp < new Date().getTime() / 1000) {
return false;
}
} catch (e) {
console.log(e); // 'Invalid token specified: Cannot read property \'replace\' of undefined'
return false;
}
return true;
};
My test doesn't work. It takes original function.
import { Auth, AuthAdmin, checkAuth, AppContent, AuthApp } from "./Auth";
import LocalStorageMock from "../../../mocks/localStorageMock";
import decode from "jwt-decode";
global.localStorage = new LocalStorageMock();
describe("auth", () => {
localStorage.setItem("token", "fake_token_user");
const token = localStorage.getItem("token");
it("allows the user to login successfully", async () => {
const decode = jest.fn(token => {
return {
exp: new Date().getTime() / 1000 - 1,
iat: 1575751766,
userData: { isAdmin: true, login: "one92tb", userId: 1 }
};
});
//const { exp } = decode(token);
expect(token).toBeDefined();
expect(checkAuth()).toBe(true) // It runs original decode function
});
});
Can someone explain to me how to solve that problem?
You can use jest.mock(moduleName, factory, options) method to mock jwt-decode module.
E.g.
index.js:
import decode from 'jwt-decode';
export const checkAuth = () => {
const token = localStorage.getItem('token');
if (!token) {
return false;
}
try {
const { exp } = decode(token);
if (exp < new Date().getTime() / 1000) {
console.log(123);
return false;
}
} catch (e) {
console.log(e);
return false;
}
return true;
};
index.spec.js:
import decode from 'jwt-decode';
import { checkAuth } from '.';
jest.mock('jwt-decode', () => jest.fn());
const mLocalStorage = {
_storage: {},
getItem: jest.fn((key) => {
return this._storage[key];
}),
setItem: jest.fn((key, value) => {
this._storage[key] = value;
}),
};
global.localStorage = mLocalStorage;
describe('auth', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('allows the user to login successfully', async () => {
localStorage.setItem('token', 'fake_token_user');
const token = localStorage.getItem('token');
decode.mockImplementationOnce((token) => {
return {
exp: new Date().getTime() / 1000 + 1,
iat: 1575751766,
userData: { isAdmin: true, login: 'one92tb', userId: 1 },
};
});
expect(token).toBe('fake_token_user');
expect(checkAuth()).toBe(true);
});
});
Unit test result with coverage report:
PASS src/stackoverflow/59233898/index.spec.js (15.687s)
auth
✓ allows the user to login successfully (11ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 61.54 | 50 | 100 | 61.54 | |
index.js | 61.54 | 50 | 100 | 61.54 | 6,12,13,16,17 |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 17.8s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59233898

Catch errors in async functions and test them via jest

I'm trying to catch errors in my async functions and test those functions using jest.
import Db from './lib/db'
const db = new Db()
export async function example (id) {
await db.connect().catch(err => console.error(err))
const Data = db.connection.collection('data')
return Data.updateOne(
{ id },
{ $set: { anything: 'else' } }
).catch(err => console.error(err))
}
But the test returns me the error:
TypeError: db.connect(...).catch is not a function
So what am I doing wrong? The testing or the catching?
test('should call mongoDB updateOne() method', async () => {
// SETUP
let Data = db.connection.collection('data')
Data.updateOne = jest.fn()
// EXECUTE
expect.assertions(1)
await example({
id: 'id'
}).then((res) => {
// VERIFY
expect(Data.updateOne).toHaveBeenCalled()
})
})
Here is the solution:
Db.ts:
import console = require('console');
export class Db {
public connection = {
collection(model: string) {
return {
async updateOne(...args) {
console.log('update one');
}
};
}
};
public async connect() {
console.log('connect db');
}
}
example.ts:
import { Db } from './Db';
const db = new Db();
export async function example(id) {
await db.connect().catch(err => console.error(err));
const Data = db.connection.collection('data');
return Data.updateOne({ id }, { $set: { anything: 'else' } }).catch(err => console.error(err));
}
example.spec.ts:
import { example } from './example';
import { Db } from './Db';
jest.mock('./Db.ts', () => {
const collectionMocked = {
updateOne: jest.fn()
};
const connectionMocked = {
collection: jest.fn(() => collectionMocked)
};
const DbMocked = {
connect: jest.fn(),
connection: connectionMocked
};
return {
Db: jest.fn(() => DbMocked)
};
});
const db = new Db();
describe('example', () => {
test('should call mongoDB updateOne() method', async () => {
const Data = db.connection.collection('data');
(db.connect as jest.MockedFunction<any>).mockResolvedValueOnce({});
(Data.updateOne as jest.MockedFunction<any>).mockResolvedValueOnce('mocked value');
const actualValue = await example('1');
expect(actualValue).toBe('mocked value');
expect(Data.updateOne).toHaveBeenCalledWith({ id: '1' }, { $set: { anything: 'else' } });
});
});
Unit test result with coverage report:
PASS src/stackoverflow/57515192/example.spec.ts
example
✓ should call mongoDB updateOne() method (6ms)
------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
------------|----------|----------|----------|----------|-------------------|
All files | 75 | 100 | 33.33 | 100 | |
example.ts | 75 | 100 | 33.33 | 100 | |
------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.336s, estimated 6s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57515192

Categories