Supertest: HTTP Assertion Testing for Express and Node.js APIs
In this tutorial, you will learn about Supertest: HTTP Assertion Testing for Express and Node.js APIs. We cover key concepts, practical examples, and best practices to help you master this topic.
Supertest is a Node.js library for testing HTTP servers, providing a high-level abstraction for making requests and asserting responses with a fluent API that integrates with Mocha, Jest, and other test frameworks.
What You'll Learn
How to test Express/Fastify APIs with Supertest, start and stop servers in test setup, chain HTTP assertions, test authentication and cookies, upload files, test WebSocket upgrades, and integrate with Mocha and Jest.
Why It Matters
Supertest is the most popular Node.js HTTP testing library with 13M+ weekly downloads. It runs tests against your actual HTTP server (no mocking) and provides a clean assertion API. DodaTech uses Supertest for all Node.js microservice API tests.
Real-World Use
A DodaTech developer adds a new endpoint to the user service. They write Supertest tests that start the Express app, send requests, validate responses, test auth middleware, and clean up after each test — all running in under 100ms per test.
flowchart LR
A["Test\nSetup"] --> B["Start\nExpress App"]
B --> C["Supertest\nRequest"]
C --> D["Assert\nResponse"]
D --> E{"Next\nTest"}
E -->|Yes| C
E -->|No| F["Teardown\nClose Server"]
style A fill:#dbeafe,stroke:#2563eb
style C fill:#bbf7d0,stroke:#16a34a
style F fill:#fecaca,stroke:#dc2626
Basic Setup and Test
const express = require("express");
const request = require("supertest");
// Create a testable Express app
const app = express();
app.use(express.json());
app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: Date.now() });
});
app.post("/api/users", (req, res) => {
const { email, name } = req.body;
if (!email || !name) {
return res.status(400).json({ error: "Email and name required" });
}
res.status(201).json({ id: 1, email, name });
});
// Test suite
describe("Health Check API", () => {
test("GET /api/health returns status ok", async () => {
const response = await request(app)
.get("/api/health")
.expect(200);
expect(response.body.status).toBe("ok");
expect(response.body).toHaveProperty("timestamp");
});
});
describe("Users API", () => {
test("POST /api/users creates a user", async () => {
const response = await request(app)
.post("/api/users")
.send({ email: "test@example.com", name: "Test User" })
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(Number),
email: "test@example.com",
name: "Test User"
});
});
test("POST /api/users without email returns 400", async () => {
const response = await request(app)
.post("/api/users")
.send({ name: "Test User" })
.expect(400);
expect(response.body.error).toContain("Email");
});
});
// Expected output:
// PASS Health Check API
// ✓ GET /api/health returns status ok (15ms)
// PASS Users API
// ✓ POST /api/users creates a user (12ms)
// ✓ POST /api/users without email returns 400 (8ms)
Authentication and Headers
const app = require("../app");
const request = require("supertest");
describe("Authenticated API", () => {
let authToken;
beforeAll(async () => {
// Login once for all tests
const response = await request(app)
.post("/api/auth/login")
.send({ email: "admin@test.com", password: "admin123" });
authToken = response.body.token;
});
test("GET /api/admin/users with valid token", async () => {
const response = await request(app)
.get("/api/admin/users")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
test("GET /api/admin/users without token returns 401", async () => {
await request(app)
.get("/api/admin/users")
.expect(401);
});
test("GET /api/admin/users with expired token returns 403", async () => {
await request(app)
.get("/api/admin/users")
.set("Authorization", "Bearer invalid_token_here")
.expect(403);
});
test("Custom header validation", async () => {
await request(app)
.get("/api/users")
.set("X-API-Version", "2.0")
.set("X-Request-Id", "test-123")
.expect(200);
});
});
File Upload Testing
const path = require("path");
describe("File Upload API", () => {
test("POST /api/upload accepts file", async () => {
const response = await request(app)
.post("/api/upload")
.attach("avatar", path.join(__dirname, "test-avatar.png"))
.expect(200);
expect(response.body).toHaveProperty("url");
expect(response.body).toHaveProperty("filename");
expect(response.body.filename).toMatch(/\.png$/);
});
test("POST /api/upload rejects large files", async () => {
// Create a 15MB buffer (exceeds limit)
const largeBuffer = Buffer.alloc(15 * 1024 * 1024);
const response = await request(app)
.post("/api/upload")
.attach("file", largeBuffer, "large-file.bin")
.expect(413); // Payload Too Large
expect(response.body.error).toContain("too large");
});
test("POST /api/upload with metadata", async () => {
const response = await request(app)
.post("/api/upload")
.field("description", "User avatar")
.field("type", "profile")
.attach("document", Buffer.from("test content"), "test.txt")
.expect(200);
expect(response.body.description).toBe("User avatar");
});
});
Database Integration Tests
const request = require("supertest");
const { app, db } = require("../app");
describe("User CRUD with Database", () => {
beforeEach(async () => {
// Seed test data
await db("users").insert([
{ email: "user1@test.com", name: "User One" },
{ email: "user2@test.com", name: "User Two" }
]);
});
afterEach(async () => {
// Clean up
await db("users").del();
});
test("GET /api/users returns seeded users", async () => {
const response = await request(app)
.get("/api/users")
.expect(200);
expect(response.body.length).toBe(2);
expect(response.body[0]).toHaveProperty("email");
});
test("DELETE /api/users/:id removes user", async () => {
const users = await db("users").select();
const firstUser = users[0];
await request(app)
.delete(`/api/users/${firstUser.id}`)
.expect(204);
const remaining = await db("users").select();
expect(remaining.length).toBe(1);
});
});
Common Mistakes
1. Starting the Server on a Real Port in Tests
Don't call app.listen() in tests. Supertest binds the app to an ephemeral port automatically. Using a fixed port causes conflicts when tests run in parallel.
2. Not Cleaning Up Database State
Tests that insert data without cleanup leave garbage in the database. Use beforeEach/afterEach with database transactions or truncation for isolated test state.
3. Hardcoding Assertion Values for Dynamic Data
IDs, timestamps, and tokens change every run. Use expect.any(Number), expect.stringMatching(/pattern/), and expect.objectContaining({...}) for dynamic fields.
4. Mixing Sync and Async Patterns
Supertest returns promises. Always await the request or return the promise. Mixing sync/async causes tests to pass before assertions run.
5. Testing Through the Full Stack When Not Needed
If you only need to test business logic, write a unit test. Supertest integration tests are for verifying HTTP handling — status codes, headers, Serialization, auth middleware.
Practice Questions
- How does Supertest start the application server in tests?
- How do you test file uploads with Supertest?
- How do you pass authentication headers in requests?
- How do you handle dynamic response data in assertions?
Answers:
- Pass the Express app instance to
request(app). Supertest binds it to an ephemeral port automatically. No need to callapp.listen()— Supertest handles the lifecycle. - Use
.attach('fieldName', filePath)for file uploads. Use.field('fieldName', value)for additional form fields. Files are handled as multipart/form-data automatically. - Use
.set('Authorization', 'Bearer token')to add headers. Supertest supports all HTTP header types. Common patterns: auth tokens, API keys, content types, and custom headers. - Use Jest/Chai matchers:
expect.any(Number)for IDs,expect.stringMatching(/^[a-f0-9]+$/)for hex strings,expect.objectContaining({ email: expect.any(String) })for partial object matching.
Challenge: Write a complete Supertest test suite for a blog API with authentication, CRUD for posts, comment creation, file upload for post images, pagination header validation, Rate Limiting, and error handling — all with database setup/teardown.
FAQ
Mini Project
Build a full Supertest test suite for an Express REST API with 8 endpoints (auth, CRUD users, posts, comments). Include: database setup/teardown, auth token management, file upload tests, pagination/headers, error case coverage, and rate limit testing. Run with Jest and generate coverage reports.
What's Next
Pytest API — write Python API tests with Pytest.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro