Reputation: 14318
My store's processAction()
function calls a private async function in a fire-and-forget manner which then does a fetch. processAction()
itself does not handle any error handling, and--in the browser--if the fetch fails, an external library handles any and all uncaught promise rejections.
So, if I mock my fetch to reject, the private function--the effects of which I am testing--will reject. Since I don't have a reference to the promise created by my async function call, I have no way of catching the rejection within the test, but the test fails because there was an unhandled rejection.
How can I tell jest to be okay with this short of calling the private function itself rather than just triggering the action that calls it?
actions.ts
const actions = {
doTheThing() {
dispatch({ type: 'DO_THE_THING' });
},
};
export default actions;
store.ts
import fetch from './fetch';
class Store {
isFetching = false;
// ...
processAction({ type, payload }: { type: string, payload: any }) {
switch (type) {
case 'DO_THE_THING':
this.fetchTheThing();
break;
}
}
private async fetchTheThing() {
try {
this.isFetching = true;
const result = await fetch(myUrl);
// ...
} finally {
this.isFetching = false;
}
}
}
export default new Store();
__mocks__/fetch.ts
let val: any;
interface fetch {
__setVal(value: any): void;
}
export default async function fetch() {
return val;
}
fetch.__setVal = function(value: any) {
val = value;
};
store.test.ts
import actions from './actions';
import store from './store';
const fetch = (require('./fetch') as import('./__mocks__/fetch')).default;
jest.mock('./fetch');
test('it sets/unsets isFetching on failure', async () => {
let rej: () => void;
fetch.__setVal(new Promise((_, reject) => rej = reject));
expect(store.isFetching).toBe(false);
Actions.doTheThing();
await Promise.sleep(); // helper function
expect(store.isFetching).toBe(true);
rej(); // <---- test fails here
await Promise.sleep();
expect(store.isFetching).toBe(false);
});
Upvotes: 0
Views: 2595
Reputation: 664971
My function calls a private async function in a fire-and-forget manner, and does not add any error handling.
Don't do that.
An external library handles any and all uncaught promise rejections. In production, I want the shell to handle it, so I do not want to handle it in the function itself.
Don't rely on this external library.
You should have your own global error handling function that you use in your function.
In production, have that error handling function simply rethrow the exception so that it gets picked up by the environment, or better, do call the shell error handling function directly if possible.
In the tests, you can mock out your own global handler, and assert that it is called with the expected arguments.
Upvotes: 1
Reputation: 222760
processAction
is synchronous and unaware of promises and this results in a dangling promise. Dangling promises should never reject because this results in unhandled rejection, which is a kind of exception. This may cause an application to crash depending on the environment. Even if exceptions are handled globally, this shouldn't be an reason to not handle errors where they are expected.
A correct way to do this is to suppress a rejection explicitly either in fetchTheThing
where it occurs:
private async fetchTheThing() {
try {
...
} catch {} finally {
this.isFetching = false;
}
}
Or in this case, it's more like processAction
that results in dangling promise:
this.fetchTheThing().catch(() => {});
Otherwise unhandled rejection event is dispatched.
Without that, it could be tested by listening for the event:
...
let onRej = jest.fn();
process.once('unhandledRejection', onRej);
rej();
await Promise.sleep();
expect(onRej).toBeCalled();
expect(store.isFetching).toBe(false);
This won't work as intended if there's already another unhandledRejection
listener, which can be expected in a good Jest setup. If this is the case, the only workaround that won't affect other tests is to reset them before the test and re-add afterwards:
let listeners;
beforeEach(() => {
listeners = process.rawListeners('unhandledRejection');
process.removeAllListeners('unhandledRejection');
});
afterEach(() => {
(typeof listeners === 'function' ? [listeners] : listeners).forEach(listener => {
process.on('unhandledRejection', listener);
});
})
This isn't recommended and should be used at own risk because this indicates a deeper problem with error handling that is not generally acceptable in properly designed JavaScript application.
Upvotes: 1