Pitfalls of Mocha's built-in Promise support
Mocha is the leader when it comes to testing frameworks in Node, and it’s great. I use it in all my projects, and I’m sure many others do too.
Mocha added support for Promises back in version 1.18.0
. Recently while testing asynchronous code written using Promises, I was using the built-in promise support instead of the old-fashioned callbacks oriented done
callback and I found my tests were resulting in false positives. So here I’m explaining what happened and how to avoid it.
Problem
It’s easy to see that there are four possible cases when testing Promises:
Case | Expectation | Actual | Expected test status |
---|---|---|---|
1 | Resolved | Resolved | Pass |
2 | Resolved | Rejected | Fail |
3 | Rejected | Rejected | Pass |
4 | Rejected | Resolved | Fail |
Case 1
1 | var promise = Promise.resolve(); |
Test status: PASS
The test expects the promise to resolve and so it does. The snippet above shows that the test passes expectedly.
Case 2
1 | var promise = Promise.reject(new Error('some-error')); |
Test status: FAIL
In the case when test expects the promise to resolve, but it doesn’t, Mocha gracefully fails the test. In the snippet above, the then
block was never executed and Mocha detected the error rejected by the code and failed the test.
Case 3
1 | var promise = Promise.reject(new Error('some-error')); |
Test status: PASS
The test expects the promise to be rejected and it is caught by the catch
block, which results in this test passing as expected.
Case 4
1 | var promise = Promise.resolve(); |
Test status: FAIL PASS
In this case, the test expects the promise to be rejected and the catch block to execute but instead, it doesn’t. The promise resolves. Mocha doesn’t throw an error here!
So while doing Negative Testing, the built-in promises support doesn’t hold up well and can result in false positives. This is not a bug and the reason behind this behavior totally makes sense, once given a careful thought, but at the very least, it’s not intuitive and causes the tests to pass even when they shouldn’t.
Solution
Good ol’ done callback
The simplest way to avoid this situation is to always handle the rejection scenarios yourself, rather than asking Mocha to do it. Here’s how:
1 | var promise = Promise.resolve(); |
Test status: FAIL
The above test will fail due to timeout as Mocha waits for done
to be called but it doesn’t, and times out eventually.
Chained then block
Another way to avoid this situation is to chain a then
block to the existing catch
block and assert that the catch was called.
1 | var expect = require('chai').expect; |
Test status: FAIL
The above test tries to assert that the catch block was called using Chai and Sinon, and fails.
If you know other elegant ways to avoid this behavior, please let me know in the comments!
❤️ code