Testing Mongoose with Jest
Jest is a JavaScript runtime developed by Facebook that is usually used for testing. 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 a different testing framework, like Mocha.
If you choose to delve into dangerous waters and test Mongoose apps with Jest, here’s what you need to know:
Recommended testEnvironment
If you are using Jest <=26
, 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. In Jest >=27
, “node” is Jest’s default testEnvironment
, so this is no longer an issue.
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. This is especially important if you’re using Jest >=25
, which stubs out process.nextTick()
.
Fake timers stub out global functions like setTimeout()
and setInterval()
, which causes problems when an underlying library uses these functions. Mongoose and 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.
If you absolutely must use timer mocks, make sure you import Mongoose before calling useFakeTimers()
:
// Fine for basic cases, but may still cause issues:
const mongoose = require('mongoose');
jest.useFakeTimers();
// Bad:
jest.useFakeTimers();
const mongoose = require('mongoose');
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()
.
A better alternative is to 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');
globalSetup
and globalTeardown
Do not use globalSetup
to call mongoose.connect()
or mongoose.createConnection()
. Jest runs globalSetup
in a separate environment, so you cannot use any connections you create in globalSetup
in your tests.
Further Reading
Want to learn how to test Mongoose apps correctly? The RESTful Web Services with Node.js and Express course on Pluralsight has a great section on testing Mongoose apps with Mocha.