
In this post, we'll run through testing a Node-MongoDB app, step by step.
You can test MongoDB using mongodb-memory-server
, an in-memory version of MongoDB that runs independently of a persistent database. A freshly spun-up mongod
process starts at roughly 7 MB of memory, providing a lightweight, self-contained environment for running tests.
Let's get going!
About MongoDB Memory Server
mongodb-memory-server
creates an in-memory MongoDB instance that can connect to any ODM, such as mongoose
. This allows tests to run in a separate, non-persistent database, ensuring isolation from your main database.
We'll set up an app with a simple Express server and a single endpoint that accepts a PUT request to save a product to a database. Let's say that the product has a name, price, and description. The test will hit the endpoint with a product object and validate the request.
Our app will connect to a MongoDB instance running in Docker, and the test will connect to the test database running in memory. We'll run integration tests with supertest
, and the test database collection will be created and destroyed for each test. Our emphasis will be on the integration tests, so the API itself will remain minimal.
To get started with the Docker image for MongoDB, run the following command:
docker run -d -p 27017:27017 --name mongodb mongo:latest
This allows you to connect to the MongoDB instance on localhost:27017
. Under normal operation, the app connects to this instance, whereas the test suite runs against an isolated in-memory database.
Feel free to follow along with this post or clone the repo from GitHub.
Prerequisites
First off, create the project folder and run npm init -y
to create a package.json
file. Then install the dependencies:
mkdir node-mongodb-testing cd node-mongodb-testing mkdir src mkdir testing npm init -y npm i -D jest supertest mongodb-memory-server @types/express @types/mongoose @types/node npm i -S express mongoose
The @types
packages are typescript definitions. You can use a tool like the TypeScript Language Server Protocol to validate your code as you write it. Many editors support the TypeScript Language Server and it is a great way to get instant feedback.
The dependencies listed under the -D
flag are for testing. The jest
package is the test runner, supertest
is the library that will make HTTP requests to the API, and mongodb-memory-server
is the in-memory database that will be used for testing.
The remaining dependencies are for the API itself. express
is the server, and mongoose
is the ODM that will connect to the MongoDB instance. The idea is to keep the API minimal, and focus on testing.
Open the package.json
file and add the following line:
"type": "module"
This tells Node to use ES modules, which are more modern than CommonJS modules.
Then, add the following scripts:
"scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand ./testing", "start": "node ./src/index.js" }
Also, configure the jest
object in the package.json
file:
"jest": { "testEnvironment": "node" }
The --runInBand
flag runs tests sequentially. This is necessary because our tests will connect to the in-memory database, and running them in parallel will cause conflicts. The testEnvironment
flag is set to node
to run the tests in the Node environment.
The --experimental-vm-modules
flag supports ES modules to keep the test code easier to digest. This is a temporary solution until Jest offers native support for ES modules.
Optionally, you can set up ESLint via the following command:
npm init @eslint/config@latest
For our project, we simply pick the default options.
Our API
Create a src/index.js
file and add the following code:
import express from "express"; export const app = express(); app.use(express.json()); app.put("/products", (req, res) => { console.log(req.body); res.status(204).send(); }); if (process.env.NODE_ENV !== "test") { app.listen(3000, () => { console.log("Server running on port 3000"); }); process.on("SIGINT", () => { console.log("Server shutting down"); process.exit(0); }); }
This is a placeholder for the API. We will differentiate between the API and the tests using the NODE_ENV
check. The API will run on port 3000, and the tests will run via supertest. Jest will set the NODE_ENV
environment variable to test
when running tests.
DB Connection
Our database connection will be handled by the mongoose
library. The app will call the connect method to initialize the connection when it starts. The connection will be closed when the app shuts down.
Our tests will perform a similar operation, but instead of connecting to the MongoDB instance running in Docker, they will connect to the in-memory database.
To support both the MongoDB instance running in Docker and the in-memory database, create a src/db.js
file and add the following code:
import mongoose from "mongoose"; import { MongoMemoryServer } from "mongodb-memory-server"; const mongodb = await MongoMemoryServer.create(); export async function connectToDatabase() { if (process.env.NODE_ENV === "test") { const uri = mongodb.getUri(); await mongoose.connect(uri); console.log("Connected to in-memory database"); return; } try { await mongoose.connect("mongodb://localhost:27017/myapp"); console.log("Connected to database"); } catch (error) { console.error("Error connecting to database", error); } } export async function disconnectFromDatabase() { if (process.env.NODE_ENV === "test") { await mongoose.connection.dropDatabase(); await mongoose.connection.close(); await mongodb.stop(); } await mongoose.disconnect(); console.log("Disconnected from database"); } export async function clearCollections() { const collections = mongoose.connection.collections; if (process.env.NODE_ENV !== "test") { throw new Error("clearCollections can only be used in test environment"); } for (const key in collections) { const collection = collections[key]; await collection.deleteMany(); } }
The mongodb
object is an instance of MongoMemoryServer
, which starts a fresh MongoDB server for testing. Note some of the logic is wrapped in an if
statement that checks the NODE_ENV
variable. This is how we differentiate between the API and the tests. The logic bifurcates and it is capable of running in both environments.
Between each test, the clearCollections
function is called to remove all documents from the collections. This is a good practice to ensure the tests are isolated from each other. Note the exception that is thrown if the function is called in the wrong environment.
After the test suite is done, the disconnectFromDatabase
function is called to close the connection. This will drop the database and allow the next test suite to run. This technique, coupled with the --runInBand
flag, will ensure the tests run smoothly.
Product Schema
The product schema is created using the mongoose
library. The schema will have three fields: name, price, and description. The schema creates a model that interacts with the database. This both stores and retrieves data and supports both the API and the tests.
To serve Mongoose data models, create a src/product.js
file and add the following code:
import mongoose from "mongoose"; const ProductSchema = new mongoose.Schema({ name: { type: String, required: true }, price: { type: Number, required: true }, description: { type: String }, }); export const ProductModel = mongoose.model("Product", ProductSchema);
Mongoose's Schema
object defines the document's structure. This adds a layer of validation to the data. Then, the model
method creates a model that interacts with the database. The model creates, reads, updates, and deletes documents.
Go back to the src/index.js
file and flesh out the rest of the API. This time, we can connect to the database and use the model to save the product. Replace the contents of the file with the following code:
import express from "express"; import { connectToDatabase, disconnectFromDatabase } from "./db.js"; import { ProductModel } from "./product.js"; export const app = express(); app.use(express.json()); app.put("/products/:id", async (req, res) => { try { const product = new ProductModel(req.body); await product.validate(); await ProductModel.updateOne( { _id: req.params.id }, { $set: req.body }, { upsert: true } ); res.status(204).send(`Product ${req.params.id} updated`); } catch (err) { console.error(err); res.status(400).send(`Error updating product ${req.params.id}`); } }); if (process.env.NODE_ENV !== "test") { await connectToDatabase(); app.listen(3000, () => { console.log("Server is running on port 3000"); }); process.on("SIGINT", async () => { await disconnectFromDatabase(); process.exit(); }); }
To encourage best programming practices, we set the _id
field as the URL parameter. This automatically enforces document uniqueness in MongoDB.
The if
block differentiates between the API and the tests and this technique is used throughout the codebase. The connectToDatabase
function is called to connect to the database when the app starts. The disconnectFromDatabase
function is called when the app shuts down, to close the connection.
If the data does not match the schema, the Mongoose validation throws an exception. This is caught in the try
block, with a 400 status code returned. The upsert
option creates a document if it does not exist.
Jest
Jest is the test runner we'll use to run our tests. The tests will be written in the testing
folder and run with the npm test
command. Because the tests use mongodb-memory-server
, the database runs entirely in memory. To maintain test isolation, collections are explicitly cleared between tests, preventing data from persisting between tests. This is why the clearCollections
function is called between each test.
Create a testing/product.test.js
file and add the following code:
import supertest from "supertest"; import { jest } from "@jest/globals"; import { app } from "../src/index.js"; import { connectToDatabase, disconnectFromDatabase, clearCollections, } from "../src/db.js"; // silence console.log and console.error jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {}); describe("Product API PUT", () => { const productId = "c3fe7eb8076e4de58d8d87c5"; beforeAll(async () => { await connectToDatabase(); }); afterAll(async () => { await disconnectFromDatabase(); }); beforeEach(async () => { await clearCollections(); }); it("should update a product", async () => { const product = { name: "Test Product", price: 100, }; await supertest(app) .put(`/products/${productId}`) .send(product) .expect(204); }); it("should return 400 if product is invalid", async () => { const product = { name: "Test Product", price: "invalid", }; await supertest(app) .put(`/products/${productId}`) .send(product) .expect(400); }); });
The console.log
and console.error
functions are mocked to silence tests, as they can otherwise be noisy. The beforeAll
hook connects to the database before the tests run. The afterAll
hook disconnects from the database after the tests run. The beforeEach
hook clears the collections before each test runs. These hooks ensure that tests are isolated from each other and allow you to run multiple test suites without conflicts.
The first test will hit the endpoint with a valid product object and validate the response. The second test will hit the endpoint with an invalid product object and validate the response. Mongoose validation will throw an exception if the data does not match the schema, and this is verified in the test.
Wrapping Up
In this post, we've run through using the MongoDB Memory Server to test your Node app.
The mongod
process started by mongodb-memory-server
is lightweight, typically starting at around 7MB when freshly spun up. This setup provides a fast, isolated environment for testing database queries without persisting data.
Happy testing!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our JavaScript Sorcery newsletter and never miss an article again.
- Start monitoring your JavaScript app with AppSignal.
- Share this article on social media
Most popular Javascript articles
Top 5 HTTP Request Libraries for Node.js
Let's check out 5 major HTTP libraries we can use for Node.js and dive into their strengths and weaknesses.
See moreWhen to Use Bun Instead of Node.js
Bun has gained in popularity due to its great performance capabilities. Let's see when Bun is a better alternative to Node.js.
See moreHow to Implement Rate Limiting in Express for Node.js
We'll explore the ins and outs of rate limiting and see why it's needed for your Node.js application.
See more

Camilo Reyes
Our guest author Camilo is a Software Engineer from Houston, Texas. He’s passionate about JavaScript and clean code that runs without drama. When not coding, he loves to cook and work on random home projects.
All articles by Camilo ReyesBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!
