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
Post a Comment