Jest manual mocking works only for the first function call - javascript

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

Related

How to mock an async axios request that is made by a helper function imported by the mobx store

I'm trying to write tests for a method in my mobX store that calls a helper method getPages that makes an asynchronous API call to fetch some data. I was successfully able to mock axios when writing tests for the getPages helper function but now that I'm testing the mobx store I can't seems to mock axios anymore.
Is this because the store file doesn't import axios directly?
Do I have to mock getPages helper function instead? If so how would I do that?
Currently the API call await axios.get('${process.env.REACT_APP_BACKEND_URL}/pages'); is returning undefined instead of the mocked value. Ultimately I just need this call to returned the mocked value.
Thank you
MobX Store
import { makeAutoObservable } from 'mobx';
import { getPages } from 'lib/api';
class DocumentStore {
pages = [];
currentPage = null;
constructor() {
makeAutoObservable(this);
}
setPages = (pages) => {
this.pages = pages;
return this.pages;
};
setCurrentPage = (page) => {
this.currentPage = page;
return this.currentPage;
};
getPages = async (documentName) => {
const pageUrls = await getPages('a-test', documentName);
this.setCurrentPage(pageUrls[0]);
this.setPages(pageUrls);
};
}
export default DocumentStore;
API call
export const getPages = async () => {
let resp;
try {
resp = await axios.get(
`${process.env.REACT_APP_BACKEND_URL}/pages`,
);
} catch (err) {
throw new Error(err);
}
if (resp?.data?.urls) {
return resp.data.urls;
} else {
throw new Error('Unable to fetch Urls');
}
};
Test File
import DocumentStore from './index';
import axios from 'axios';
const TEST_PAGES = [
{
url: 'URL1',
},
{
url: 'URL2',
},
{
url: 'URL3',
},
];
const FETCH_PAGES_MOCK = {
data: {
urls: TEST_PAGES,
},
};
jest.mock('axios');
describe('DocumentStore', () => {
describe('getPages', () => {
axios.get.mockResolvedValue(FETCH_PAGES_MOCK);
it('fetches pages', () => {
const store = new DocumentStore();
expect(store.pages.length).toEqual(0);
store.getPages();
expect(store.pages.length).toEqual(3);
});
});
});
Mock indirect dependencies(axios.get()) and mock direct dependencies(getPages from lib/api module), both of them are ok. Depends on which modules you want as a unit. I prefer smaller units, only mock modules that the tested code directly depends on.
Too large a unit will make the test more complicated and difficult to locate the problem. The huge unit is close to higher level testing, such as integration testing, e2e testing.
For your case, you forgot to use await when calling the store.getPages() method.
E.g.
index.test.js:
jest.mock('axios');
describe('DocumentStore', () => {
describe('getPages', () => {
axios.get.mockResolvedValue(FETCH_PAGES_MOCK);
it('fetches pages', async () => {
const store = new DocumentStore();
expect(store.pages.length).toEqual(0);
await store.getPages('test');
expect(store.pages.length).toEqual(3);
expect(axios.get).toBeCalled();
});
});
});

automatically perform action on Promise method resolve

I have a MobX data store, called BaseStore that handles the status of an API request, telling the view to render when request is in progress, succeeded, or failed. My BaseStore is defined to be:
class BaseStore {
/**
* The base store for rendering the status of an API request, as well as any errors that occur in the process
*/
constructor() {
this._requestStatus = RequestStatuses.NOT_STARTED
this._apiError = new ErrorWrapper()
}
// computed values
get requestStatus() {
// if there is error message we have failed request
if (this.apiError.Error) {
return RequestStatuses.FAILED
}
// otherwise, it depends on what _requestStatus is
return this._requestStatus
}
set requestStatus(status) {
this._requestStatus = status
// if the request status is NOT a failed request, error should be blank
if (this._requestStatus !== RequestStatuses.FAILED) {
this._apiError.Error = ''
}
}
get apiError() {
// if the request status is FAILED, return the error
if (this._requestStatus === RequestStatuses.FAILED) {
return this._apiError
}
// otherwise, there is no error
return new ErrorWrapper()
}
set apiError(errorWrapper) {
// if errorWrapper has an actual Error, we have a failed request
if (errorWrapper.Error) {
this._requestStatus = RequestStatuses.FAILED
}
// set the error
this._apiError = errorWrapper
}
// actions
start = () => {
this._requestStatus = RequestStatuses.IN_PROGRESS
}
succeed = () => {
this._requestStatus = RequestStatuses.SUCCEEDED
}
failWithMessage = (error) => {
this.apiError.Error = error
}
failWithErrorWrapper = (errorWrapper) => {
this.apiError = errorWrapper
}
reset = () => {
this.requestStatus = RequestStatuses.NOT_STARTED
}
}
decorate(BaseStore, {
_requestStatus: observable,
requestStatus: computed,
_apiError: observable,
apiError: computed,
})
That store is to be extended by all stores that consume API layer objects in which all methods return promises. It would look something like this:
class AppStore extends BaseStore {
/**
* #param {APIObject} api
**/
constructor(api) {
super()
this.api = api
// setup some observable variables here
this.listOfData = []
this.data = null
// hit some initial methods of that APIObject, including the ones to get lists of data
api.loadInitialData
.then((data) => {
// request succeeded
// set the list of data
this.listOfData = data
}, (error) => {
// error happened
})
// TODO: write autorun/reaction/spy to react to promise.then callbacks being hit
}
save = () => {
// clean up the data right before we save it
this.api.save(this.data)
.then(() => {
// successful request
// change the state of the page, write this.data to this.listOfData somehow
}, (error) => {
// some error happened
})
}
decorate(AppStore, {
listOfData : observable,
})
Right now, as it stands, I'd end up having to this.succeed() manually on every Promise resolve callback, and this.failWithMessage(error.responseText) manually on every Promise reject callback, used in the store. That would quickly become a nightmare, especially for non-trivial use cases, and especially now that we have the request status concerns tightly coupled with the data-fetching itself.
Is there a way to have those actions automatically happen on the resolve/reject callbacks?
Make an abstract method that should be overridden by the subclass, and call that from the parent class. Let the method return a promise, and just hook onto that. Don't start the request in the constructor, that only leads to problems.
class BaseStore {
constructor() {
this.reset()
}
reset() {
this.requestStatus = RequestStatuses.NOT_STARTED
this.apiError = new ErrorWrapper()
}
load() {
this.requestStatus = RequestStatuses.IN_PROGRESS
this._load().then(() => {
this._requestStatus = RequestStatuses.SUCCEEDED
this._apiError.error = ''
}, error => {
this._requestStatus = RequestStatuses.FAILED
this._apiError.error = error
})
}
_load() {
throw new ReferenceError("_load() must be overwritten")
}
}
class AppStore extends BaseStore {
constructor(api) {
super()
this.api = api
this.listOfData = []
}
_load() {
return this.api.loadInitialData().then(data => {
this.listOfData = data
})
}
}
const store = new AppStore(…);
store.load();
MobX can update data that is asynchronously resolved. One of the options is to use runInAction function
example code:
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// after await, modifying state again, needs an actions:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
You can read more in official documentation: Writing asynchronous actions

Calling 'created' method inside Jest tests on Vue.js

I'm testing my Activity.vue component on Vue.js with Jest.
My Activity.vue component calls the initializeTable() method inside created() in order to initialize my tableRows property.
The problem is, when I run my tests, Jest does not call 'created()' method and, for that reason, it does not initialize my tableRows property, then the test says that my tableRows property is equal to [] (original state).
How could I call created() method when I'm testing with Jest?
I've already tried to call it explicitly inside the test (i.e. wrapper.vm.$created()), but to no avail.
Would anyone know how to solve it?
I give my code below.
Thank you in advance.
Activity.spec.js
import { shallowMount } from '#vue/test-utils'
import Activity from '#/views/Activity.vue'
import api from '#/assets/js/api'
const expectedTableRows = [...]
const $t = () => {}
api.getActivity = jest.fn(
() => Promise.resolve({
data: [...]
})
)
describe('Activity.vue', () => {
it('renders vm.tableRows in created()', () => {
const wrapper = shallowMount(Activity, {
mocks: { $t }
})
// wrapper.vm.$created()
expect(wrapper.vm.tableRows).toEqual(expectedTableRows)
})
})
Activity.vue
import api from '#/assets/js/api'
export default {
data () {
return {
tableRows: []
}
},
created () {
this.initializeTable()
},
methods: {
initializeTable () {
this.loading = true
api.getActivity().then(response => {
this.tableRows = response.data
}).catch(error => {
this.errorBackend = error
}).finally(() => {
this.loading = false
})
}
}
}
EDIT 1: Solution
As Husam Ibrahim described, the function is asynchronous, because of that, I needed to refactor the test using the following tips and now I was able to fix it.
I give the correct code below:
import flushPromises from 'flush-promises' // <--
...
describe('Activity.vue', () => {
it('renders wm.tableRows after created()', async () => { // <--
const wrapper = shallowMount(Activity, {
mocks: { $t }
})
await flushPromises() // <--
expect(wrapper.vm.tableRows).toEqual(expectedTableRows)
})
})

Inconsitant mergeMap behaviour

I am currently working on a file uploading method which requires me to limit the number of concurrent requests coming through.
I've begun by writing a prototype to how it should be handled
const items = Array.from({ length: 50 }).map((_, n) => n);
from(items)
.pipe(
mergeMap(n => {
return of(n).pipe(delay(2000));
}, 5)
)
.subscribe(n => {
console.log(n);
});
And it did work, however as soon as I swapped out the of with the actual call. It only processes one chunk, so let's say 5 out of 20 files
from(files)
.pipe(mergeMap(handleFile, 5))
.subscribe(console.log);
The handleFile function returns a call to my custom ajax implementation
import { Observable, Subscriber } from 'rxjs';
import axios from 'axios';
const { CancelToken } = axios;
class AjaxSubscriber extends Subscriber {
constructor(destination, settings) {
super(destination);
this.send(settings);
}
send(settings) {
const cancelToken = new CancelToken(cancel => {
// An executor function receives a cancel function as a parameter
this.cancel = cancel;
});
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.next([null, resp.data]))
.catch(e => this.next([e, null]));
}
next(config) {
this.done = true;
const { destination } = this;
destination.next(config);
}
unsubscribe() {
if (this.cancel) {
this.cancel();
}
super.unsubscribe();
}
}
export class AjaxObservable extends Observable {
static create(settings) {
return new AjaxObservable(settings);
}
constructor(settings) {
super();
this.settings = settings;
}
_subscribe(subscriber) {
return new AjaxSubscriber(subscriber, this.settings);
}
}
So it looks something like this like
function handleFile() {
return AjaxObservable.create({
url: "https://jsonplaceholder.typicode.com/todos/1"
});
}
CodeSandbox
If I remove the concurrency parameter from the merge map function everything works fine, but it uploads all files all at once. Is there any way to fix this?
Turns out the problem was me not calling complete() method inside AjaxSubscriber, so I modified the code to:
pass(response) {
this.next(response);
this.complete();
}
And from axios call:
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.pass([null, resp.data]))
.catch(e => this.pass([e, null]));

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