Hello everyone,
It’s been a while since I last shared something—back when I was deep into React Hook Form. After wrapping up my frontend, I shifted focus to gluing all pieces together and building full MERN stack applications (more on that another time...).
While writing unit tests for the backend of one of these apps, I ran into a subtle but important realization: mocking dependencies and clearing mock state alone is not always enough to guarantee proper test isolation—especially when testing Express routing logic.
The scenario
Consider the following Express router:
// product.route.ts
import express from "express";
import {
createProduct,
deleteProduct,
getAllProducts,
updateProduct,
} from "../controllers/product.controller.js";
const router = express.Router();
/**
* Endpoint: /api/products
*/
router.get("/", getAllProducts);
router.post("/", createProduct);
router.put("/:id", updateProduct);
router.delete("/:id", deleteProduct);
export default router;
Suppose our goal is simple: verify that each HTTP method is correctly wired to its corresponding controller. We are not interested in the controller logic itself, so mocking the controllers is a perfectly reasonable approach.
Since my project used Typescript and ESM, I preferred Vitest for testing. We can mock the controllers like this:
vi.mock("../controllers/product.controller.js", () => ({
getAllProducts: vi.fn((req, res) =>
res.status(200).json({ message: "getAllProducts called" }),
),
createProduct: vi.fn((req, res) =>
res.status(201).json({ message: "createProduct called" }),
),
updateProduct: vi.fn((req, res) =>
res.status(200).json({ message: "updateProduct called" }),
),
deleteProduct: vi.fn((req, res) =>
res.status(200).json({ message: "deleteProduct called" }),
),
}));
And before each test, we typically clear mocks:
vi.clearAllMocks();
This does reset:
call counts (.mock.calls)
recorded arguments
mock results (.mock.results)
However, this is where a common misconception appears.
What clearing mocks does not reset
Clearing mocks does not reset the runtime state of your application. Particularly, it doesn't:
Recreate the Express app or router instance
Reset Express’ internal routing stack
Undo mutations to req, res, or app objects made by previous tests
Reset middleware side effects
Reload cached modules from Node’s module system
Note that Express routers are stateful objects. Once a router is created and attached to an app, its middleware stack and handlers persist for the lifetime of that app instance.
Why this matters in tests
If you reuse the same Express app across tests, subtle state can leak:
A previous test may mutate
req.headers,req.body, orres.localsMiddleware may attach data to
reqthat subsequent tests unexpectedly inherit and misuseOrder-dependent behavior can emerge when tests rely on shared app state
This can lead to tests that:
Pass or fail depending on execution order
Fail intermittently
Give false confidence about route behavior
Mock isolation alone cannot protect you from this class of problems.
The correct approach: reset runtime state
For modules with stateful dependencies (such as Express apps) the solution is twofold:
Reset mock state (call counts, return values, spies)
Recreate the runtime environment (new app instance per test)
import express, { type Express } from "express";
import router from "./routes";
describe("Product routes", () => {
let app: Express;
const createTestApp = () => {
const app = express();
app.use(express.json());
app.use("/api", router);
return app;
};
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
app = createTestApp();
});
// tests go here
});
By constructing a fresh Express app for each test, you guarantee:
No shared routing or middleware state
Predictable request/response objects
True test isolation
Takeaway
Mocking is about behavior substitution. Test isolation is about state control.
When testing Express routes, clearing mocks addresses only half the problem. To achieve reliable and deterministic tests, you must also reset—or recreate—the application runtime itself.
Feel free to drop your own experiences and lessons below.
Happy testing guys👋
Top comments (0)