Reputation: 1330
I'm working on adding test coverage to a Node project I'm working on using Jest. The code I'm testing is throwing errors within promises resulting in an UnhandledPromiseRejectionWarning
message being logged to the console.
While writing tests, I can pretty easily identify these issues and resolve them, but these warnings aren't actually causing Jest to mark the tests as failed, so our CI won't catch it. I've searched around for any suggestions and haven't found much.
I did find in Node's documentation that you can catch these warnings and handle them...
process.on('unhandledRejection', (error) => {
throw error; // Or whatever you like...
});
So it seems like it would be pretty straightforward to add this code into my test cases. After all, an Error
thrown within the test should cause the test to fail...
describe('...', () => {
it('...', () => {
process.on('uncaughtRejection', (error) => {
throw error;
});
// the rest of my test goes here
});
});
Unfortunately the behavior I'm seeing is that the error does get thrown, but Jest doesn't catch it and fail the test. Instead, Jest crashes with this error and the tests don't continue to run. This isn't really desirable, and seems like incorrect behavior.
Throwing an error outside of the uncaughtRejection
handler works as expected: Jest logs the thrown error and fails the test, but doesn't crash. (i.e. the test watcher keeps watching and running tests)
Upvotes: 31
Views: 21670
Reputation: 1179
If this is react code you are testing, you can intercept any unhandled errors by attaching an event handler to JSDOM's VirtualConsole's jsdomError
event. This will let you spy the event and also prevent it from crashing jest.
let jsdomError;
beforeEach(() => {
jsdomError = jest.fn();
window._virtualConsole.on('jsdomError',jsdomError);
});
afterEach(() => {
window._virtualConsole.off('jsdomError',jsdomError);
});
test(() => {
/* after you test everything */
expect(jsdomError).not.toHaveBeenCalled();
});
I wrapped this up into an ErrorBoundary component that I use in my @testing-library/react
tests.
export default class TestBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: false };
}
jsdomError = (err) => {
this.componentDidCatch(err.detail, {});
};
componentDidMount() {
window._virtualConsole.on('jsdomError', this.jsdomError);
}
componentWillUnmount() {
window._virtualConsole.off('jsdomError', this.jsdomError);
}
componentDidCatch(error, { componentStack }) {
const { onError } = this.props;
this.setState({ error, componentStack });
if (onError) {
onError(error);
} else {
console.error(error);
}
}
render() {
const { children, fallback } = this.props;
const { error, componentStack } = this.state;
if (!error) return children;
if (isValidElement(fallback)) {
return cloneElement(fallback, this.state);
}
if (typeof fallback === 'function') {
return createElement(fallback, this.state);
}
return fallback || (
<div data-testid="error">
{error.stack || error.message}
{'\n'}
Components:
{'\n'}
{componentStack}
</div>
);
}
}
Upvotes: 1
Reputation: 5046
Include the following content in Jest's setupFiles
:
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', reason => {
throw reason
})
// Avoid memory leak by adding too many listeners
process.env.LISTENING_TO_UNHANDLED_REJECTION = true
}
Courtesy of stipsan in https://github.com/facebook/jest/issues/3251#issuecomment-299183885.
Upvotes: 9
Reputation: 17022
module:
export function myPromise() {
return new Promise((resolve, reject) => {
const error = new Error('error test');
reject(error);
});
}
test:
import { myPromise } from './module';
it('should reject the promise', () => {
expect.assertions(1);
const expectedError = new Error('error test');
myPromise().catch((error) => {
expect(error).toBe(expectedError);
});
Upvotes: 2
Reputation: 659
From the node documentation site we can see that The process object is an instance of EventEmitter
.
Using the emit function from process
we can trigger the errors like uncaughtRejection
and uncaughtException
programmatically when needed.
it("should log the error", () => {
process.emit("unhandledRejection");
...
const loggerInfo = jest.spyOn(logger, "info");
expect(loggerInfo).toHaveBeenCalled();
});
Upvotes: 0
Reputation: 1981
Not sure if this helps, but you can also assert for promise rejections as such
index.js
module.exports = () => {
return Promise.reject('it didnt work');
}
index.spec.js
const thing = require('../src/index');
describe('rejected promise', () => {
it('should reject with a reason', ()=> {
return expect(thing()).rejects.toEqual('it didnt work');
});
});
Upvotes: -4
Reputation: 15780
The way I've approached this is very much tied into the way I write my functions - basically, any function that uses promises should return a promise. This allows whatever code calls that function to handle catching errors in any way it sees fit. Note that this is my approach and I'm not going to claim this is the only way to do things.
For example... Imagine I'm testing this function:
const myFunction = () => {
return doSomethingWithAPromise()
.then(() => {
console.log('no problems!');
return true;
});
};
The test will look something like this:
describe('...', () => {
it('...', () => {
return myFunction()
.then((value) => {
expect(value).toBe(true);
});
});
});
Which works great. Now what happens if the promise is rejected? In my test, the rejected promise is passed back to Jest (because I'm returning the result of my function call) and Jest can report on it.
If, instead, your function does not return a promise, you might have to do something like this:
const myOtherFunction = () => {
doSomethingWithAPromise()
.then(() => {
console.log('no problems!');
return true;
})
.catch((err) => {
// throw the caught error here
throw err;
});
};
Unlike the example above, there is no (direct) way for Jest to handle a rejected promise because you're not passing the promise back to Jest. One way to avoid this might be to ensure there is a catch
in the function to catch & throw the error, but I haven't tried it and I'm not sure if it would be any more reliable.
Upvotes: 6