Middleware Testing Patterns — Complete Implementation Guide
In this tutorial, you will learn about Middleware Testing Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Middleware testing patterns verify that each middleware function behaves correctly in isolation, handles edge cases, and integrates properly with the request-response pipeline.
What You'll Learn
By the end of this tutorial, you will test middleware functions using mock objects, verify error propagation, test conditional logic, and benchmark middleware performance.
Why It Matters
Untested middleware causes production bugs that are hard to debug. DodaTech tests every middleware function with unit and integration tests before deploying to production.
Real-World Use
Doda Browser's authentication middleware has 30+ unit tests covering valid tokens, expired tokens, missing headers, malformed tokens, and role-based access edge cases.
Middleware Testing Learning Path
flowchart LR
A[Third-Party Middleware] --> B[Middleware Testing]
B --> C[Unit Tests]
B --> D[Integration Tests]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Unit Testing Middleware
Unit tests verify middleware in isolation by creating mock req, res, and next objects. This approach is fast and does not require starting a server.
const assert = require("assert");
function loggingMiddleware(req, res, next) {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
}
function createMocks() {
const req = { method: "GET", url: "/test" };
const res = {};
let nextCalled = false;
const next = () => { nextCalled = true; };
return { req, res, next, nextCalled };
}
// Test
const { req, res, next, nextCalled } = createMocks();
loggingMiddleware(req, res, next);
assert.strictEqual(nextCalled, true);
console.log("Test passed: next() was called");
Testing Authentication Middleware
Authentication middleware needs more complex mocks that include headers and can test both success and failure paths.
const assert = require("assert");
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (!token) {
res.statusCode = 401;
res.body = { error: "No token" };
return;
}
if (token !== "Bearer valid-token") {
res.statusCode = 401;
res.body = { error: "Invalid token" };
return;
}
req.user = { id: 1, role: "user" };
next();
}
// Test missing token
const req1 = { headers: {} };
const res1 = { statusCode: 200 };
authMiddleware(req1, res1, () => {});
assert.strictEqual(res1.statusCode, 401);
assert.deepStrictEqual(res1.body, { error: "No token" });
// Test valid token
const req2 = { headers: { authorization: "Bearer valid-token" } };
const res2 = {};
let nextCalled2 = false;
authMiddleware(req2, res2, () => { nextCalled2 = true; });
assert.strictEqual(nextCalled2, true);
assert.deepStrictEqual(req2.user, { id: 1, role: "user" });
console.log("All auth tests passed");
Integration Testing with Supertest
Integration tests start the Express server and send real HTTP requests through the full middleware pipeline, verifying end-to-end behavior.
const express = require("express");
const request = require("supertest");
const assert = require("assert");
function createTestApp() {
const app = express();
app.use((req, res, next) => {
req.timestamp = Date.now();
next();
});
app.get("/test", (req, res) => {
res.json({ timestamp: req.timestamp });
});
return app;
}
const app = createTestApp();
request(app)
.get("/test")
.expect(200)
.then(response => {
assert(response.body.timestamp !== undefined);
console.log("Integration test passed: timestamp is present");
});
Testing Error Middleware
Error middleware requires passing an error object and verifying it is handled correctly, with appropriate status codes and messages.
const assert = require("assert");
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
res.statusCode = statusCode;
res.body = {
error: err.message,
type: err.constructor.name
};
}
// Test with custom error
const customErr = new Error("Not found");
customErr.statusCode = 404;
const req = {};
const res = { statusCode: 200 };
errorHandler(customErr, req, res, () => {});
assert.strictEqual(res.statusCode, 404);
assert.strictEqual(res.body.error, "Not found");
// Test with generic error
const genericErr = new Error("Server crash");
const res2 = { statusCode: 200 };
errorHandler(genericErr, req, res2, () => {});
assert.strictEqual(res2.statusCode, 500);
console.log("Error handler tests passed");
Common Mistakes
Not testing error paths — Testing only the happy path misses critical bugs. Always test invalid inputs, missing data, and error conditions.
Using real server instances for unit tests — Unit tests should use mocks. Starting a server for every test makes tests slow and brittle.
Not resetting mocks between tests — Shared mutable state between tests causes false positives or failures. Create fresh mocks for each test.
Testing implementation details instead of behavior — Test what middleware does (sets status, calls next, modifies req), not how it does it.
Ignoring async error paths — Test that async middleware properly forwards errors to
next()and does not crash.
Practice Questions
What is the difference between unit and integration testing for middleware? Unit tests test middleware in isolation with mocks. Integration tests send real HTTP requests through the full pipeline.
How do you test that middleware calls
next()with an error? Pass a spy function asnextand assert it was called with an Error object.Why should you not start a real server for unit tests? Starting a server is slow and introduces dependencies. Mock-based tests run in milliseconds and do not need ports.
Challenge: Write tests for middleware that adds a request ID if one is not present.
function requestId(req, res, next) {
if (!req.id) {
req.id = crypto.randomUUID();
}
next();
}
// Test: req without id gets one, req with id keeps it
FAQ
Mini Project
Build a test suite for a complete middleware pipeline including authentication, validation, and error handling middleware.
const assert = require("assert");
function validateAge(req, res, next) {
const age = Number(req.query.age);
if (isNaN(age)) return res.status(400).json({ error: "Age must be a number" });
if (age < 0 || age > 150) return res.status(400).json({ error: "Invalid age" });
req.validAge = age;
next();
}
// Test 1: Valid age
const req1 = { query: { age: "25" } };
const res1 = { statusCode: 200, json: (d) => { res1.body = d; } };
validateAge(req1, res1, () => {});
assert.strictEqual(req1.validAge, 25);
// Test 2: Invalid age
const req2 = { query: { age: "abc" } };
const res2 = { statusCode: 200, json: (d) => { res2.body = d; res2.statusCode = 400; } };
validateAge(req2, res2, () => {});
assert.strictEqual(res2.statusCode, 400);
// Test 3: Out of range
const req3 = { query: { age: "200" } };
const res3 = { statusCode: 200, json: (d) => { res3.body = d; res3.statusCode = 400; } };
validateAge(req3, res3, () => {});
assert.strictEqual(res3.statusCode, 400);
console.log("All validation tests passed");
What's Next
Now that you understand middleware testing, explore optimizing middleware for high throughput. Then learn about securing your middleware pipeline.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro