I'm currently creating a new React component for one of our projects and I'm pretty much stuck with writing a proper test for it. I've read quite a few docs and blog posts and more but I can't seem to get it running.
TL;DR
To me, it seems the Promise is not executed. When I run the test with a debugger, it won't stop in the Promise's function and neither in the then() function. It will, however, stop in the then/catch functions in the test itself.
The Code
So, the component is actually fairly simple. For the time being it is supposed to search for a location via an API. The test for it looks like this:
import axios from 'axios';
import React from 'react';
import {shallowWithIntl} from "../../../Helpers/react-intl-helper";
import Foo from "../../../../src/Components/Foo/Foo";
import {mount} from "enzyme";
const queryTerm = 'exampleQueryTerm';
const locationAgs = 'exampleLocationKey';
const fakeLocationObject = {
search: '?for=' + queryTerm + '&in=' + locationAgs
};
jest.mock('axios', () => {
const exampleLocations = [{
data: {"id": "expected-location-id"}
}];
return {
get: jest.fn().mockReturnValue(() => {
return Promise.resolve(exampleLocations)
})
};
});
let fooWrapper, instance;
beforeEach(() => {
global.settings = {
"some-setting-key": "some-setting-value"
};
global.URLSearchParams = jest.fn().mockImplementation(() => {
return {
get: function(param) {
if (param === 'for') return queryTerm;
else if (param === 'in') return locationAgs;
return '';
}
}
});
fooWrapper = shallowWithIntl(<Foo location={fakeLocationObject} settings={ global.settings } />).dive();
instance = fooWrapper.instance();
});
it('loads location and starts result search', function() {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(axios.get).toHaveBeenCalled();
expect(fooWrapper.state('location')).not.toBeNull();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
});
});
So, as you can see the test is supposed to call searchLocation on the Foo component instance, which returns a Promise object, as you can (almost) see in its implementation.
import React, { Component } from 'react';
import { injectIntl } from "react-intl";
import {searchLocationByKey} from "../../Services/Vsm";
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
location: null,
searchingLocation: false,
searchParams: new URLSearchParams(this.props.location.search)
};
}
componentDidUpdate(prevProps) {
if (!prevProps.settings && this.props.settings) {
this.searchLocation();
}
}
searchLocation() {
this.setState({
searchingLocation: true
});
const key = this.state.searchParams.get('in');
return searchLocationByKey(key)
.then(locations => {
this.setState({ location: locations[0], searchingLocation: false })
})
.catch(error => console.error(error));
}
render() {
// Renders something
};
}
export default injectIntl(Foo);
Enter searchLocationByKey:
function requestLocation(url, resolve, reject) {
axios.get(url).then(response => {
let locations = response.data.map(
location => ({
id: location.collectionKey || location.value,
rs: location.rs,
label: location.label,
searchable: location.isSearchable,
rawData: location
})
);
resolve(locations);
}).catch(error => reject(error));
}
export const searchLocationByKey = function(key) {
return new Promise((resolve, reject) => {
let url = someGlobalBaseUrl + '?regional_key=' + encodeURIComponent(key);
requestLocation(url, resolve, reject);
});
};
The Problem
This is the output of the test:
Error: expect(received).toBe(expected)
Expected value to be (using ===):
[Error: expect(received).not.toBeNull()
Expected value not to be null, instead received
null]
Received:
null
I have to admit that I'm pretty new to Promises, React and JavaScript testing, so I might have mixed up several things. As I wrote above, it seems that the Promise is not executed properly. When debugging, it will not stop in the then() function defined in Foo.searchLocation. Instead, apparently, both the then() and catch() functions defined in the test are executed.
I've spent way too much time on this issue already and I'm clueless on how to go on. What am I doing wrong?
Update 1: done() function
As El Aoutar Hamza pointed out in an answer below, it is possible to pass a function (usually called "done") to the test function. I've done exactly this:
it('loads location and starts result search', function(done) {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(fooWrapper.state('location')).not.toBeNull();
done();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
});
});
But I end up getting this error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Inside requestLocation you are trying to access response.data, and when mocking axios.get, you are returning a Promise resolved with an array ! you should instead return a Promise resolved with an object with data property (that contains the array).
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({
data: [{ "id": "expected-location-id" }]
}))
}));
Another point is that when testing asynchronous code, the test will finish before even calling the callbacks, that's why you should consider providing your test an argument called done, that way, jest will wait until the done callback is called.
describe('Foo', () => {
it('loads location and starts result search', done => {
expect.assertions(1);
return instance
.searchLocation()
.then((data) => {
expect(fooWrapper.state('location')).not.toBeNull();
done();
})
.catch((error) => {
expect(fooWrapper.state('location')).toBe(error);
done();
});
});
});
So like I mentioned in my latest comment under El Aoutar Hamza's answer, I have found a solution thanks to a colleague who was able to help me.
It seems that it is not possible to return the Promise from Foo.searchLocation on to the test. What we needed to do was to wrap the code getting and handling the Promise from searchLocationByKey into yet another Promise, which looks like this:
import React, { Component } from 'react';
import { injectIntl } from "react-intl";
import {searchLocationByKey} from "../../Services/Vsm";
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
location: null,
searchingLocation: false,
searchParams: new URLSearchParams(this.props.location.search)
};
}
componentDidUpdate(prevProps) {
if (!prevProps.settings && this.props.settings) {
this.searchLocation();
}
}
searchLocation() {
this.setState({
searchingLocation: true
});
const key = this.state.searchParams.get('in');
return new Promise((resolve, reject) => {
searchLocationByKey(key)
.then(locations => {
this.setState({ location: locations[0], searchingLocation: false });
resolve();
})
.catch(error => {
console.error(error));
reject();
}
});
}
render() {
// Renders something
};
}
export default injectIntl(Foo);
Only then was Jest able to properly hook into the promise and everything worked as I expected it to be in the first place.
I still didn't understand why the promise cannot simply be returned and needs to be wrapped in another Promise, though. So if someone has an explanation for that it would be greatly appreciated.
Related
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 :)
Edited Question with vazsonyidl suggestions applied
I have to write unit tests for a function similar to this one:
import {External} from 'ExternalModule';
async functionA(){
this.functionB().then((data) => {
External.functionC(options);
console.log("Reached1");
}).catch((data) => {
const { OnError = "" } = data || {}
if(OnError) {
External.functionC(anotherOptions);
console.log("Reached2");
}
})
}
functionB() {
return new Promise(() => {
});
}
As functionC belongs to another module, I placed a mock of it in the _mocks_folder:
//_mocks_/ExternalModule.ts
export var External: ExternalClass = {
functionC(){}
}
class ExternalClass{
constructor(){};
functionC(){};
}
I have mocked functionB in two diferent ways for testing the then and the catch :
it("should test then block", () => {
functionB = jest.fn(() => {return Promise.resolve()});
const functionSpy = jest.spyOn(ExternalModule.External, 'functionC');
void functionA().then(() => {
expect(functionSpy).not.toHaveBeenCalled();
});
})
it("should test catch block", () => {
const err = { OnError: "Error" };
functionB = jest.fn(() => {return Promise.reject(err)});
const functionSpy = jest.spyOn(ExternalModule.External, 'functionC');
void functionA().then(() => {
expect(functionSpy).not.toHaveBeenCalled();
});
})
What I am trying to do is expect that functionC was called and called with the correct params, but the test is always passing even if I test if functionC was not called.
What am I doing wrong?
Jest does not wait for the async code to complete before doing assertions.
You can use the following function:
const waitForPromises = () => new Promise(setImmediate);
to force Jest to wait for promises to complete before continuing like so:
it("does something", async () => {
promiseCall();
await waitForPromises();
expect(something).toBe(something)
});
I think when this function catch error, this error should have an 'OnError' property so the functionC can run.
const { OnError = "" } = data || {}
if(OnError) {
ExternalClass.functionC(anotherOptions);
}
change you response error data to return Promise.reject({OnError: '404'}) may solve this problem.
Because you are not providing it to your class.
The following code is working for me:
class A {
async functionA() {
this.functionB().then((data) => {
this.functionC(); // It woll log aaa here, you need this one.
}).catch((data) => {
const {OnError = ''} = data || {};
if (OnError) {
console.log('onerror');
}
});
}
functionB() {
return new Promise(() => {
});
}
functionC() {
return 2;
}
}
describe('a', () => {
it('test', () => {
const a = new A();
a.functionB = jest.fn(() => Promise.resolve());
const functionBSpy = jest.spyOn(a, 'functionC');
void a.functionA().then(() => {
expect(functionBSpy).toHaveBeenCalledTimes(1);
});
});
});
Hope this helps, any comment appreciated.
As you provided no information about your functionB I mocked something that may suitable for you.
Your original problem is that Jest does not wait for your callbacks to settle. It does the assertion although, even if your function calls happen later, Jest will not recognise them and says that no call ever occurred.
There are several docs available, for example Jest's one here
Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}
I've mocked a custom XHR wrapper that I've written (utils/xhr.js) using Jest manual mocking feature and the issue that I'm having is that only the first XHR call is tracked:
utils/xhr.js
let xhr = {
get: function(params) { /* XHR get implementation */ }
post: function(params) { /* XHR post implementation */ }
};
export default xhr;
utils/__mocks__/xhr.js - the mock implementation
let xhr = jest.genMockFromModule('./../xhr');
module.exports = xhr;
Dashboard.api.js - the XHR calls for the dashboard component
// ...
getTrendingEntities: (days, maxItems) => {
return xhr.get({url: '/api/aggregator/riskEntities/' + days + '/' + maxItems})
.then((response) => {
companyIds = parseCompanies(response.body);
return xhr.post({
url: '/api/entities/specific-companies',
data: companyIds
});
}).then((response) => {
let companies = parseCompaniesData(response.body);
return Promise.resolve(companies);
});
}
// ...
TrendingEntitiesPod.jsx - the React component that uses Dashboard.api.js
class TrendingEntitiesPod extends React.Component {
// ...
componentWillMount() {
this.loadData(this.props.days)
}
componentWillReceiveProps(nextProps) {
if (this.props.days != nextProps.days) {
this.loadData(nextProps.days);
}
}
loadData(days) {
this.setState({loading: true});
api.getTrendingEntities(days, DASHBOARD_PODS_ENTRIES_NUMBER)
.then((companies) => {
this.setState({
companies: companies,
loading: false
});
});
}
render() { // ... }
}
StartPage.TrendingEntitiesPod.spec.js - the test file
import React from 'react';
import { mount } from 'enzyme';
import xhr from './../../../utils/xhr';
jest.mock('./../../../utils/xhr');
import TrendingEntitiesPod from './../Dashboard/components/TrendingEntitiesPod/TrendingEntitiesPod.jsx';
describe('StartPage.TrendingEntitiesPod:', () => {
let wrapper = null;
beforeAll(() => {
xhr.get.mockReturnValueOnce(Promise.resolve({body: trendingEntities}));
xhr.post.mockReturnValueOnce(Promise.resolve({body: trendingEntitiesData}));
xhr.get.mockReturnValue(Promise.resolve({body: trendingEntityPestleData}));
wrapper = mount(<TrendingEntitiesPod days={30} />);
});
test('...stubbing works', () => {
expect(xhr.get.mock.calls.length).toBe(1);
expect(xhr.post.mock.calls.length).toBe(1); // returns false - why??
});
});
It seems that mounting with Enzyme a component that does multiple AJAX calls and multiple state changes, in the components' first lifecycle phases, makes the unit tests run first and afterwards the state changes are reflected.
This is due to the asynchronous nature of the AJAX and setState() calls.
A fix that I found for this is to add a done callback in the beforeAll block which finishes on setTimeout(done, 0):
beforeAll((done) => {
xhr.get.mockReturnValueOnce(Promise.resolve({body: trendingEntities}));
xhr.post.mockReturnValueOnce(Promise.resolve({body: trendingEntitiesData}));
xhr.get.mockReturnValue(Promise.resolve({body: trendingEntityPestleData}));
wrapper = mount(<TrendingEntitiesPod days={30} />);
// Delay the execution of tests, at the end, after the component
// has been mounted, all XHR, setState and render calls are done.
setTimeout(done, 0);
});
Another way to deal with this issue, as it seems that Jest doesn't play well when doing setTimeout() all the time, is to not test the component's state after all initial requests have done. You can test the individual methods that do the XHR requests (and data parsing logic/component state change). E.g.:
AirlineCompanies.jsx
class AirlineCompanies extends React.Component {
// ...
loadData(numberOfCompanies) {
this.setState({loading: true});
return api.getAirlineCompanies(numberOfCompanies)
.then((companies) => {
this.setState({
companies: companies,
loading: false
});
})
.catch((error) => {
utils.logError(error, 'AirlineCompanies.loadData():');
this.setState({loading: false});
});
}
// ...
}
AirlineCompanies.spec.js
let component = mount(<AirlineCompanies />).instance();
let airlineCompaniesData = [ /*... mock companies data ...*/ ];
it('...should have the state changed and eight AirlineCompany components after data is loaded', (done) => {
xhr.get.mockReturnValueOnce(Promise.resolve({body: airlineCompaniesData}));
component.loadData(30).then(() => {
expect(wrapper.state('companies')).toEqual(airlineCompaniesData);
expect(wrapper.find(AirlineCompany).length).toBe(8);
done();
});
});
I am trying to add unit test cases to my redux actions.
I have tried this, this & this
I am using thunk, promise-middleware in my actions
one of my action is like this
export function deleteCommand(id) {
return (dispatch) => {
dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
}
unit test for this is
import configureMockStore from "redux-mock-store"
const middlewares = [thunk, promiseMiddleware()];
const mockStore = configureMockStore(middlewares)
it('creates DELETE_COMMAND_FULFILLED after deleting entry', (done) => {
nock('http://localhost:8081/')
.post('/delete',{
_id: 1
})
.reply(200, {})
const expectedActions = [{
type: 'DELETE_COMMAND_FULFILLED',
payload: {}
}]
const store = mockStore({
command: {
commands: []
}
})
store.dispatch(actions.deleteCommand({_id: 1}).then(function () {
expect(store.getActions())
.toEqual(expectedActions)
done();
})
})
I am using nock,redux-mock-store configured with thunk, promise middleware
It gives then of undefined
then I changed the action to return the promise, then I got unhandled promise reject exception, I added a catch to the action dispatch call.
Now I am getting Network Error because nock is not mocking the call.
Also tried moxios, changed axios to isomorphic-fetch, whatwg-fetch. Doesn't seem to be working
Where am I doing it wrong?.
I know It's from 2017,
But maybe someone will have the same issue.
So the problem is that the arrow function inside deleteCommand returns undefined.
That's why you get
It gives then of undefined
TLDR:
if you using redux-thunk with redux-promise-middleware
You must return the inner dispatch
here is the official redux-promise-middleware docs
The solution is very simple :
Option 1.
Add return inside the arrow function
export function deleteCommand(id) {
return (dispatch) => {
return dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
}
Option 2.
Remove the curly braces from the arrow function
export function deleteCommand(id) {
return (dispatch) =>
dispatch({
type: 'DELETE_COMMAND',
payload: axios.post(`${Config.apiUrl}delete`, { _id: id })
})
}
Now you can do
store.deleteCommand(<some id>).then(...)
In general arrow functions and return
Return a plain object
// option 1
const arrowFuncOne = () => {
return {...someProps}
}
// option 2
const arrowFuncTwo = () => ({
...someProps
})
// This means that you returning an expression
//same as
// return a result of other function
const arrowFuncCallFunOne = () => callSomeOtherFunc()
// or
const arrowCallFunkTwo = () => {return callSomeOtherFunc()}
// However
// wrong
const arrowCallFunkNoReturn = () => {callSomeOtherFunc()}
// in this case the curly braces are just the body of the function
// and inside this body there is no return at all