Skip to main content

The Easiest Way to Test Node.js Apps with MongoDB: Without Breaking Your Production Database

 

The Easiest Way to Test Node.js Apps with MongoDB: Without Breaking Your Production Database


The Easiest Way to Test Node.js Apps with MongoDB: Without Breaking Your Production Database

I’ve been there, staring at my Node.js app, praying my tests don’t mess up the production MongoDB database. One wrong move, and poof, real user data could vanish.

I was in search of a way to test safely, without risking the live database. Today we are going to see how to use mongodb-memory-server to create isolated, in-memory MongoDB instances for testing. It’s fast, reliable, and keeps your production environment untouched.

Let’s get started.

What Makes mongodb-memory-server Special?

Going through an article, I found a tool that allowed me to spin up a temporary MongoDB instance in memory. No external database, no cleanup headaches. It’s perfect for writing tests that don’t bleed into the live data.

Setting Up the Project

Today we are going to build a simple Node.js app. The stack includes Express for the server, Mongoose for MongoDB interaction, and Jest for testing. The focus is on isolated, repeatable tests.

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

Using above commands we are setting up Express for the server, Mongoose for MongoDB models, and mongodb-memory-server for testing.

We are using Jest and Supertest handle the testing framework and HTTP assertions.

Let’s understand why we are using these tools? Express is straightforward, Mongoose simplifies MongoDB queries, and Jest makes testing feel like a conversation with your code.

Next, tweak your package.json to use ES modules and add test scripts:

{  
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand ./testing",
"start": "node ./src/index.js"
},
"jest": {
"testEnvironment": "node"
}
}

Notice the --runInBand flag we are using in the test script. It allows us to make sure that tests run one at a time. It is important to prevent conflicts because our tests use an in-memory database. Parallel execution would mess things up.

We are also using the testEnvironment flag for jest to use Node.js environment for the tests.

The test script also uses the --experimental-vm-modules flag. This flag enables ES module support. It keeps test code clean and readable. This is a workaround until Jest fully supports ES modules.

Building the Server

Create a src/index.js file for a basic Express server:

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);
});
}

In our server code above we are using a NODE_ENV check to conditionally run the server as per Node.js environment. The API runs on port 3000. Tests run through Supertest. Jest sets the NODE_ENV to test during test execution.

Setting Up Database

Let’s move further with setting up the database with our Node.js application. We are going to use Mongoose to handle our database connection. Our app will use the connect method which will set up the database connection on startup. The connection closes when the app shuts down. Tests do something similar but connect to an in-memory database.

Create src/db.js file to connect to database:

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();
}
}

In above example we are using a MongoMemoryServer instance to start a MongoDB server for testing. To separate API behavior from test behavior we are using the NODE_ENV environment variable here. The code adapts to run in either environment.

We have an interesting function here: clearCollections . It allows to wipe all documents from collections. This keeps tests isolated and independent.

We are usingdisconnectFromDatabase function on test completion to drop the database and close the connection.

Setting Up a Data Model

Let’s start with defining our schema. It includes three fields: name, price, and description. The data model handles data storage and retrieval for both the API and tests.

To set up the data model, create a src/product.js file with this 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);

Let’s update the server src/index.js file:

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();
});
}

If the data doesn’t fit the schema, Mongoose validation raises an error. The try block catches it and returns a 400 status code. The upsert option creates a new document if none exists.

Setting up Testing With Jest

We are going to use Jest for executing tests. We can simply run the tests with the npm test command. Here we are using mongodb-memory-server, which means the database operates fully in memory.

To ensure test isolation, collections are cleared between tests. This prevents data from lingering, thanks to the clearCollections function called after each test.

Create a testing/product.test.js file with this 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);
});
});

In above example we are silencing the unnecessary noise. The database connection is established before tests start and closes after tests finish. To make sure tests run in isolation it clears the collections before each test.

The first test sends a valid product object to the endpoint and checks the response. The second test sends an invalid product object and verifies the response. Mongoose validation triggers an error for schema mismatches, which the test confirms.

Final Takeaway

We built a Node.js app with MongoDB and tested it using mongodb-memory-server. Each test runs in an isolated, in-memory database, keeping your production data safe.

Thank you. Let’s meet again with another cool guide on JavaScript.

Comments

Popular posts from this blog

CSS only Click-handlers You Might not be using, but you should

  You’re building a simple website, a good-looking landing page with a “See More” button. Instinctively, you reach for JavaScript to handle the button click event. But wait — what if I told you that CSS alone could do the job? Yes. CSS is often underestimated, but it can handle click interactions without JavaScript. In this guide, you’ll learn how to create CSS-only click handlers using the :target pseudo-class, and explore scenarios where this approach makes perfect sense. The :target Pseudo-Class CSS offers several pseudo-classes that let you style elements based on different states ( :hover , :focus , :checked ). But there’s one you might not have used before —  :target . The :target pseudo-class applies styles to an element when its ID matches the fragment identifier in the URL (the part after # ). This behavior is commonly seen when clicking an anchor link that jumps to a section on the same page. Here’s a simple example : <a href="#contact">Go to Contact</...

Sharpen Your Front-End Skills: Quick HTML, CSS & React Interview Challenges

  The source of this image is Chat GPT based on writing! Are you preparing for front-end developer interviews and looking for practical, hands-on ways to improve your HTML, CSS, and React skills? Whether you’re a beginner aiming to build confidence or an experienced developer brushing up on UI skills, small, targeted challenges can make a huge difference. In this article, I’ll walk you through some of the best free and low-cost resources that offer real-world front-end tasks — perfect for interview prep, portfolio building, and daily practice. 1. Frontend Mentor frontendmentor.io Frontend Mentor is one of the most popular platforms for hands-on HTML, CSS, and JavaScript challenges. You get beautifully designed templates (in Figma or image formats) and are asked to bring them to life using clean code. The platform offers difficulty levels ranging from newbie to expert, and it’s perfect for practicing responsiveness and semantic HTML. Bonus : You can even filter for React-based ...

6 Essential JavaScript Concepts Every Developer Should Understand

It’s the only language I’ve used where [] == ![] it's true and where you can, typeof null and somehow get 'object' . But despite all its quirks (and there are many), there are a few core concepts that make life with JS not just easier, but saner. This isn’t some computer science flex. These are practical concepts that, once you understand them, make you write better, cleaner, and less buggy code. 1. Hoisting  Before you rage at your variables being undefined , understand this: JS hoists variable and function declarations to the top of their scope. But —  and this is important  —  only the declarations , not the assignments. Why? Because JS reads it like: This is also why let and const behave differently — they’re hoisted too, but live in the “Temporal Dead Zone” until declared. 2. Closures Closures are like little memory vaults for your functions. They allow functions to remember variables from the scope they were created in, even after that scope has gone. Why care? T...