Testing Mongoose with Jest

Jest is a client-side JavaScript testing library developed by Facebook. It was one of the libraries affected by Facebook's licensing scandal in 2017. Because Jest is designed primarily for testing React applications, using it to test Node.js server-side applications comes with a lot of caveats. We strongly recommend using Mocha instead. If you choose to delve into dangerous waters and test Mongoose apps with Jest, here's what you need to know:

Do not use Jest's default jsdom test environment when testing Mongoose apps, unless you are explicitly testing an application that only uses Mongoose's browser library.

The jsdom test environment attempts to create a browser-like test environment in Node.js, and it comes with numerous nasty surprises like a stubbed setTimeout() function that silently fails after tests are finished. Mongoose does not support jsdom in general and is not expected to function correctly in the jsdom test environment.

To change your testEnvironment to Node.js, add testEnvironment to your jest.config.js file:

module.exports = {
  testEnvironment: 'node'
};

Timer Mocks

Absolutely do not use timer mocks when testing Mongoose apps. Fake timers stub out global functions like setTimeout() and setInterval(), which causes problems when an underlying library uses these functions. The MongoDB Node.js driver uses these functions for deferring work until the next tick of the event loop and for monitoring connections to the MongoDB server.

Mongoose devs have already refactored out code to avoid using setImmediate() to defer work to the next tick of the event loop, but we can't reasonably ensure that every library Mongoose depends on doesn't use setImmediate().

Mongoose uses nextTick(), which Jest's underlying dependency explicitly doesn't stub by default. But, process.nextTick() isn't the same as setImmediate() because of microtasks vs macrotasks, so underlying libraries like the MongoDB driver may use setImmediate().

To work around this, create your own wrapper around setTimeout() and stub that instead using sinon.

// time.js
exports.setTimeout = function() {
  return global.setTimeout.apply(global, arguments);
};

// Tests
const time = require('../util/time');
const sinon = require('sinon');
sinon.stub(time, 'setTimeout');

Further Reading

Want to learn more about testing Mongoose apps? The RESTful Web Services with Node.js and Express course on Pluralsight has a great section on testing Mongoose apps with Mocha.