redux test with jest - javascript

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

Related

Jest mocking a mongodb record in a service

I'm trying to test a service in Nestjs which is responsible for getting one record out of a mongo database, using Jest.
As per common convention, when writing unit tests that test services we can mock a record that would sit in a database.
I'm trying the following implementation:
import { Test } from '#nestjs/testing';
import { QuestionsService } from './questions.service';
import { CreateQuestionRequestDto } from './dto/create-question-request.dto';
import { getModelToken } from '#nestjs/mongoose';
import { UpdateQuestionRequestDto } from './dto/update-question-request.dto';
import { NotFoundException } from '#nestjs/common';
import { DuplicateQuestionRequestDto } from './dto/duplicate-question-request.dto';
const testQuestion: CreateQuestionRequestDto = {
data: {
createdBy: { id: 0, name: '' },
lanugageTexts: undefined,
options: undefined,
status: undefined,
type: undefined,
entityId: 1,
propertyId: 'propHash1',
companyId: 1,
entityType: 'announcement',
},
};
describe('QuestionsService', () => {
let questionService: QuestionsService;
let findOne: jest.Mock;
let findOneAndUpdate: jest.Mock;
let find: jest.Mock;
beforeEach(async () => {
// save = jest.fn();
findOne = jest.fn();
findOneAndUpdate = jest.fn();
find = jest.fn();
const module = await Test.createTestingModule({
providers: [
QuestionsService,
{
provide: getModelToken('Question'),
useValue: {}
}
]
})
.compile();
questionService = await module.get<QuestionsService>(QuestionsService);
});
it('should be defined', () => {
expect(questionService).toBeDefined();
});
/**
* Question Get
*/
describe('when getting a question', () => {
describe('and the questionId does not exist', () => {
beforeEach(() => {
findOne.mockReturnValue(undefined);
})
it('should throw a NotFound exception', async () => {
const response = await questionService.get('announcement', 9136500000);
expect(response).toThrow(NotFoundException);
});
});
describe('and the questionId exists', () => {
beforeEach(() => {
findOne.mockResolvedValue(Promise.resolve(testQuestion));
});
it('should update the correct question', async() => {
const response = await questionService.get('announcement', 1);
expect(response).toMatchObject(updatedTestQuestion);
});
});
});
});
When I run this test I get the following error message.
● QuestionsService › when getting a question › and the questionId does not exist › should throw a NotFound exception
TypeError: this.questionModel.find is not a function
52 | const data: Question[] = [];
53 | const questions = await this.questionModel
> 54 | .find(
| ^
55 | { entityType: entityType, entityId: entityId, status: QuestionStatus.ACTIVE },
56 | { answers: 0 },
57 | {
at QuestionsService.get (questions/questions.service.ts:54:14)
at Object.<anonymous> (questions/questions.spec.ts:128:56)
The service method I'm testing is.
async get(entityType: string, entityId: number): Promise<any> {
const data: Question[] = [];
const questions = await this.questionModel
.find(
{ entityType: entityType, entityId: entityId, status: QuestionStatus.ACTIVE },
{ answers: 0 },
{
sort: { _id: -1 },
limit: 1,
}
)
.exec();
if (!questions.length) {
throw new NotFoundException();
}
questions.forEach((question) => {
data.push(question);
});
return { data };
}
find() is the mongoose method that fetches the record from the database. I believe for the test I need to somehow include these methods I'm using in the service and mock them but I cannot find one clear answer.

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

redux-thunk structure and test side effects

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

How can I get response of this.$store.dispatch on the vue.js 2?

My component is like this :
<script>
export default{
props:['search','category','shop'],
...
methods: {
getVueItems: function(page) {
this.$store.dispatch('getProducts', {q:this.search, cat:this.category, shop: this.shop, page:page}).then(response => {
console.log(response)
this.$set(this, 'items', response.body.data)
this.$set(this, 'pagination', response.body)
}, error => {
console.error("this is error")
})
},
...
}
}
</script>
The ajax call getProducts method on the product.js module
The product.js module is like this :
import { set } from 'vue'
import product from '../../api/product'
import * as types from '../mutation-types'
// initial state
const state = {
list: {}
}
// actions
const actions = {
getProducts ({ commit,state }, payload)
{
product.getProducts( payload,
data => {
let products = data
commit(types.GET_PRODUCTS,{ products });
},
errors => {
console.log('error load products ')
}
)
}
}
// mutations
const mutations = {
[types.GET_PRODUCTS] (state, { products }) {
state.list = {}
products.data.forEach(message => {
set(state.list, message.id, message)
})
}
}
export default {
state,
actions,
mutations
}
Then, the module call getProducts method again on the product.js api
The product.js api is like this :
import Vue from 'vue'
import Resource from 'vue-resource'
Vue.use(Resource)
export default {
// api to get filtered products
getProducts (filter, cb, ecb = null ) {
Vue.http.post(window.Laravel.baseUrl+'/search-result',filter)
.then(
(resp) => cb(resp.data),
(resp) => ecb(resp.data)
);
}
}
When executed, I check on the console, the response not show. The response undefined
How can I solve the error?
UPDATE
If I use normal ajax like this :
<script>
export default{
props:['search','category','shop'],
...
methods: {
getVueItems: function(page) {
const q = this.search
const cat = this.category
const shop = this.shop
this.$http.get('search-result?page='+page+'&q='+q+'&cat='+cat+'&shop'+shop).then((response) => {
console.log(JSON.stringify(response))
this.$set(this, 'items', response.body.data)
this.$set(this, 'pagination', response.body)
});
},
...
}
}
</script>
It works. It get the response
But, Why when I use vuex store, it does not work?
You should return an Promised in your actions.
Try:
// actions
const actions = {
getProducts ({ commit,state }, payload)
{
return new Promise((resolve, reject) => {
product.getProducts( payload,
data => {
let products = data
commit(types.GET_PRODUCTS,{ products });
resolve(data)
},
errors => {
console.log('error load products ')
reject(errors)
}
)
})
}
}
or simply, you could just pass return Vue.http.post() up.

Vuex: Testing actions with API calls

I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.
I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!

Categories