Error: expected mock function to have been called - onclick Jest enzyme - javascript

Not being able to get the following test to pass...
Sorry for putting up a lot of code....
I was able get some other click events working but I am stuck with this one at the moment
Getting the following message:
"expect(jest.fn()).toHaveBeenCalled()
expected mock function to have been called."
here is the click event under Render method
className={!this.state.name || !this.state.label || this.state.valueStore === null ? `add-custom-field-button disabled` : `add-custom-field-button`}
id="test-addclick"
onClick={() => {this.onAddClick()}}
>
Create Field
</button>
here is onAddClick method:
onAddClick = () => {
let obj = this.props.selectedFormJSON;
this.addValueAttribute().then(()=>{
obj.FORM_COLUMN.push(
{
Control: this.state.control,
CreateBy: this.props.user.userId,
Datatype: this.state.datatype,
Form_UID: this.props.selectedFormJSON.Form_UID,
Help: this.state.help,
ValueStore: this.state.valueStore
}
)
this.props.updateSelectedFormJSON(obj);
if(!this.props.isNewForm && this.state.valueStore) {
this.props.patchForm().then((res)=>{
if(res.Forms[0] && (res.Forms[0].Code === '200' || res.Forms[0].Code===200)) {
toast(<div>Attribute Added Successfully!</div>, {type: toast.TYPE.SUCCESS,position: toast.POSITION.TOP_LEFT})
} else {
toast(<div>Failed to Add Attribute!</div>, {type: toast.TYPE.ERROR,position: toast.POSITION.TOP_LEFT})
}
});
} else if(this.state.valueStore) {
this.props.postForm().then((res)=>{
if(res.Forms[0] && (res.Forms[0].Code === '201' || res.Forms[0].Code===201)) {
toast(<div>Attribute Added Successfully!</div>, {type: toast.TYPE.SUCCESS,position: toast.POSITION.TOP_LEFT})
} else {
toast(<div>Failed to Add Attribute!</div>, {type: toast.TYPE.ERROR,position: toast.POSITION.TOP_LEFT})
}
})
}
this.props.closeModal();
})
}
addValueAttribute = () => {
return new Promise((resolve, reject) => {
if(this.state.valueStore) {
let {valueTables, valueDatatypes, service} = this.state;
let body = {
Name: this.state.name,
TableName: this.props.selectedFormJSON.Entity,
Datatype: this.state.datatype,
ChangeType: 'I'
}
fetch(service.URL+'/VALUE_ATTRIBUTE', { headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': service.subscription_key,
},
method: 'POST',
credentials: 'include',
body: JSON.stringify(body),
})
.then((res) => {
res.status === 201 && resolve();
})
.catch(() => {
reject();
})
} else {
//Not a value attr
resolve()
}
})
}
Here is how I am trying to test it: using jest/enzyme. I have been using the same set up for some other click events and it has been working. Unable to figure out for the following:
it("should call onAddClick", async () => { // use an async test method
baseProps. closeModal.mockClear();
baseProps. updateSelectedFormJSON.mockClear();
const instance = wrapper.instance();
const spy = jest.spyOn(instance, 'addValueAttribute'); // spy on addValueAttribute...
spy.mockImplementation(() => Promise.resolve()) // give any callbacks queued in PromiseJobs a chance to run
wrapper.find('#test-addclick').at(0).simulate('click'); // simulate click
expect(baseProps.updateSelectedFormJSON).toHaveBeenCalled(); // SUCCESS
expect(baseProps.closeModal).toHaveBeenCalled(); // SUCCESS
});

addValueAttribute is expensive so you will want to mock it to resolve immediately.
addValueAttribute is a class field so you will need to mock it using the component instance.
When onAddClick is called it will call this.addValueAttribute which will be mocked to immediately return. This will cause the Promise callback in then to get added to the PromiseJobs queue. Jobs in this queue run after the current message completes and before the next message begins.
This means that the callback that calls this.props.updateSelectedFormJSON and this.props.closeModal is queued in the PromiseJobs queue when the click handler returns and the test continues.
At this point you need to pause your test to give the callback queued in PromiseJobs a chance to run. The easiest way to do that is to make your test function async and call await Promise.resolve(); which will essentially queue the rest of the test at the end of the PromiseJobs queue and allow any jobs already in the queue to run first.
Putting it all together, here is a simplified version of your code with a working test:
import * as React from 'react';
import { shallow } from 'enzyme';
class Comp extends React.Component {
onAddClick = () => {
this.addValueAttribute().then(() => {
this.props.updateSelectedFormJSON();
this.props.closeModal();
})
}
addValueAttribute = () => {
return new Promise((resolve) => {
setTimeout(resolve, 100000); // does something computationally expensive
});
}
render() {
return (<button onClick={this.onAddClick}>Create Field</button>);
}
}
it("should call onAddClick", async () => { // use an async test method
const props = {
updateSelectedFormJSON: jest.fn(),
closeModal: jest.fn()
}
const wrapper = shallow(<Comp {...props} />);
const instance = wrapper.instance();
const spy = jest.spyOn(instance, 'addValueAttribute'); // spy on addValueAttribute...
spy.mockResolvedValue(); // ...and mock it to immediately resolve
wrapper
.find('button')
.at(0)
.simulate('click'); // simulate click
await Promise.resolve(); // give any callbacks queued in PromiseJobs a chance to run
expect(props.updateSelectedFormJSON).toHaveBeenCalled(); // SUCCESS
expect(props.closeModal).toHaveBeenCalled(); // SUCCESS
});

Related

Jest Unit Testing function that calls a second one that returns a promise

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

How do I setup this JS code to do better testing?

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();
}

Async Await is not working as expected: ReactJS

I have this problem where in, user must be redirected to respective dashboards on successful log-in. Say user has accounts in two profiles "p1" and "p2" . After Sign-In success, I am making fetch API to see if user has entries the corresponding profiles.
1)Say if a user has entries in p1, I need to redirect him to "p1" dashboard ;
2) if entries in p2, then redirect him to "p2" dashboard.
3)If no entries are present neither in p1 or p2, then redirect to configuration page where he can make some entries.
4) If in both profile, then user will be asked to select a respective dashboard
Now In my case, the code that I have written is not working as expected. Even though I have no accounts in "p2" but have accounts in "p1" its taking me to configuration page. Can someone help what is wrong in this code?
Note that fetch call just works fine! It gives me array of items. If no items present it returns an empty array
// On login
handleLogin = async () => {
// after successful signin
await Promise.all([
this.setDashboard("p1"),
this.setDashboard("p2"),
]);
this.navigateToDashboards();
};
setDashboard = async (profile) => {
let response, value;
let type = profile;
if (type === "p1") {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP1: value });
} else {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP2: value });
}
};
fetchUserAccounts = async (type) => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
}
);
return response.json();
} catch (error) {
console.log(error);
}
};
navigateToDashboards = () => {
let { isP1, isP2 } = this.state;
if (isP1 && isP2) {
// CODE FOR PROMPTING WHICH DASHBOARD TO GO
} else if (!isP1 && !isP2) {
this.props.history.push("/configpage");
} else if (isP1) {
this.props.history.push("/p1dashboard");
} else if (isP2) {
this.props.history.push("/p2dashboard");
}
};
There are some issues with the code you wrote above and they probably go deeper than we can see in the snippet – why having the logic for the dashboard type on the client-side instead of having it sent from the server?
There is also cloudType that is not specified in the setDashboard method.
I do not know what you are trying to achieve so I'm guessing – you should probably use fetchUserAccounts inside componentDidMount and save the response in the state.
I've came up with something like this with one TODO: in the code:
class X extends React.Component {
constructor(props) {
super(props);
this.state = {
p1: null,
p2: null,
};
}
async componentDidMount() {
const [p1, p2] = await Promise.all([
this.fetchUserAccounts('p1'),
this.fetchUserAccounts('p1'),
]);
// NOTE: setState has a second parameter, which is a callback – this way you make sure that the state has been updated – read more here:
// https://reactjs.org/docs/react-component.html#setstate
this.setState(s => ({ ...s, p1, p2 }), this.navigateToDashboards);
}
fetchUserAccounts = async (type) => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
},
});
return response.json();
} catch (error) {
console.log(error);
}
};
navigateToDashboards = () => {
const { history } = this.props;
const { p1, p2 } = this.state;
// TODO: Implement your logic how you decide
if (isP1 && isP2) {
// CODE FOR PROMPTING WHICH DASHBOARD TO GO
return;
}
if (isP1) return history.push('/p1dashboard');
if (isP2) return history.push('/p2dashboard');
return history.push('/configpage');
}
}
If this does not work at least have a look at the code structure. There are places where you do not require the else statement. Use the function version of the setState. Make use of object/array desctrucuring. You should also probably read about what is false and true in the JS world based on the !!response && response.length ? true : false; lines – or maybe there is something I'm missing out here.
The problem is you're using async / await wrong.
await works on promises- you give it a promise, and it waits for it to resolve / reject. If you do not give it a promise- its like passing it an immediately resolved promise.
Your setDashboard function does not reutrn a promise, so what happens is that this code:
await Promise.all([
this.setDashboard("p1"),
this.setDashboard("p2"),
]);
actually resolves immediately, but the setDashboard functions are not executed up until this.navigateToDashboards();. why? because they are defined as async, which puts them in event loop. Then comes in the await, but it immediately resolves as the functions inside does not return a promise (well, it does, because it is async, but its like writing return promise.resolve() as the first line- so the await does nothing only placing the function in event loop)- and the code continues to execute in synchronous fashion until it reaches the end of the function (e.g- executing this.navigateToDashboards();, and only then goes to event loop and execute the setDashboards functions.
On the same subject, response = await this.fetchUserAccounts(type); line also has redundant await, because fetchUserAccounts() does not return a promise.
To correctly use async / await- make sure your async function return a promise, for example:
setDashboard = async (profile) => {
return new Promise( resolve, reject => {
let response, value;
let type = profile;
if (type === "p1") {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP1: value });
resolve();
} else {
response = await this.fetchUserAccounts(cloudType);
value = !!response && response.length ? true : false;
this.setState({ isP2: value });
resolve();
}
})
};
as for fetchUserAccount:
fetchUserAccounts = async (type) => {
return new Promise( resolve, reject => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
}
);
resolve(response.json());
} catch (error) {
console.log(error);
reject(error);
}
}
};
Notice that you will now have to handle promise rejections using .catch, otherwise you'll get unhandled promise rejection error.

ES7 promises and awaiting async function which loops forever in background

This might be a special case:
I want to read from a queue (AWS SQS), which is done by making a call which waits a few secs for messages, and then resolves - and call again and again in a loop as long as you want to process that queue (it checks a flag every time).
This means that I have a consume function which is running as long as the app is active, or the queue is unflagged.
And I also have a subscribe function used for subscribing to a queue - which is supposed to resolve as soon as it knows that the consumer is able to connect to the queue. Even though this functions calls the consumer which keeps running and does not return until the queue is unflagged.
It gives me some challenges - do you have any tips on how to solve this with modern JS and async/await promises? I keep in mind this code is running in a React web app, not in node.js.
I basically just want the await subscribe(QUEUE) call (which comes from the GUI) to resolve as soon as it's sure that it can read from that queue. But if it cannot, I want it to throw an error which is propagated to the origin of the subscribe call - which means that I have to await consume(QUEUE), right?
Update:
Some untested draft code has been added (I don't want to spend more time making it work if I'm not doing the right approach) - I thought about sending success and failure callback to the consuming function, so it can report a success as soon as it gets the first valid (but possibly empty) response from the queue, which makes it store the queue url as a subscription - and unsubscribe if as the queue poll fails.
Since I'm setting up several queue consumers they should not be blocking anything but just work in the background
let subscribedQueueURLs = []
async function consumeQueue(
url: QueueURL,
success: () => mixed,
failure: (error: Error) => mixed
) {
const sqs = new AWS.SQS()
const params = {
QueueUrl: url,
WaitTimeSeconds: 20,
}
try {
do {
// eslint-disable-next-line no-await-in-loop
const receivedData = await sqs.receiveMessage(params).promise()
if (!subscribedQueueURLs.includes(url)) {
success()
}
// eslint-disable-next-line no-restricted-syntax
for (const message of receivedData.Messages) {
console.log({ message })
// eslint-disable-next-line no-await-in-loop
eventHandler && (await eventHandler.message(message, url))
const deleteParams = {
QueueUrl: url,
ReceiptHandle: message.ReceiptHandle,
}
// eslint-disable-next-line no-await-in-loop
const deleteResult = await sqs.deleteMessage(deleteParams).promise()
console.log({ deleteResult })
}
} while (subscribedQueueURLs.includes(url))
} catch (error) {
failure(error)
}
}
export const subscribe = async (entityType: EntityType, entityId: EntityId) => {
const url = generateQueueURL(entityType, entityId)
consumeQueue(
url,
() => {
subscribedQueueURLs.push(url)
eventHandler && eventHandler.subscribe(url)
},
error => {
console.error(error)
unsubscribe(entityType, entityId)
}
)
}
I ended up solving it like this - maybe not the most elegant solution though...
let eventHandler: ?EventHandler
let awsOptions: ?AWSOptions
let subscribedQueueUrls = []
let sqs = null
let sns = null
export function setup(handler: EventHandler) {
eventHandler = handler
}
export async function login(
{ awsKey, awsSecret, awsRegion }: AWSCredentials,
autoReconnect: boolean
) {
const credentials = new AWS.Credentials(awsKey, awsSecret)
AWS.config.update({ region: awsRegion, credentials })
sqs = new AWS.SQS({ apiVersion: '2012-11-05' })
sns = new AWS.SNS({ apiVersion: '2010-03-31' })
const sts = new AWS.STS({ apiVersion: '2011-06-15' })
const { Account } = await sts.getCallerIdentity().promise()
awsOptions = { accountId: Account, region: awsRegion }
eventHandler && eventHandler.login({ awsRegion, awsKey, awsSecret }, autoReconnect)
}
async function handleQueueMessages(messages, queueUrl) {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
// eslint-disable-next-line no-restricted-syntax
for (const message of messages) {
if (!eventHandler) {
return
}
// eslint-disable-next-line no-await-in-loop
await eventHandler.message({
content: message,
queueUrl,
timestamp: new Date().toISOString(),
})
const deleteParams = {
QueueUrl: queueUrl,
ReceiptHandle: message.ReceiptHandle,
}
// eslint-disable-next-line no-await-in-loop
await sqs.deleteMessage(deleteParams).promise()
}
}
export async function subscribe(queueUrl: QueueUrl) {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
const initialParams = {
QueueUrl: queueUrl,
WaitTimeSeconds: 0,
MessageAttributeNames: ['All'],
AttributeNames: ['All'],
}
const longPollParams = {
...initialParams,
WaitTimeSeconds: 20,
}
// Attempt to consume the queue, and handle any pending messages.
const firstResponse = await sqs.receiveMessage(initialParams).promise()
if (!subscribedQueueUrls.includes(queueUrl)) {
subscribedQueueUrls.push(queueUrl)
eventHandler && eventHandler.subscribe(queueUrl)
}
handleQueueMessages(firstResponse.Messages, queueUrl)
// Keep on polling the queue afterwards.
setImmediate(async () => {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
try {
do {
// eslint-disable-next-line no-await-in-loop
const received = await sqs.receiveMessage(longPollParams).promise()
handleQueueMessages(received.Messages, queueUrl)
} while (sqs && subscribedQueueUrls.includes(queueUrl))
} catch (error) {
eventHandler && eventHandler.disconnect()
throw error
}
})
}

Promise not being executed in Jest test

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.

Categories