Skip to content

Supertest: HTTP Assertion Testing for Express and Node.js APIs

DodaTech Updated 2026-06-28 6 min read

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

  1. How does Supertest start the application server in tests?
  2. How do you test file uploads with Supertest?
  3. How do you pass authentication headers in requests?
  4. How do you handle dynamic response data in assertions?

Answers:

  1. Pass the Express app instance to request(app). Supertest binds it to an ephemeral port automatically. No need to call app.listen() — Supertest handles the lifecycle.
  2. Use .attach('fieldName', filePath) for file uploads. Use .field('fieldName', value) for additional form fields. Files are handled as multipart/form-data automatically.
  3. 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.
  4. 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

Can Supertest test non-Express apps?

Yes, Supertest works with any Node.js HTTP server (Express, Fastify, Koa, Hapi, plain http.createServer). Pass the app object that the framework creates.

How do I test HTTPS endpoints?

Supertest supports HTTPS by passing the app to request.agent(app) with SSL options. For most HTTP tests, Supertest auto-negotiates with the server.

What is the difference between Supertest and superagent?

Superagent is the HTTP client library. Supertest wraps superagent with convenience methods for testing: .expect(statusCode), automatic app binding, and assertion helpers.

How do I mock external APIs in Supertest tests?

Use nock (HTTP mocking library) to intercept outgoing HTTP requests from your app. Nock prevents real external calls and returns mock responses.

Can I test GraphQL endpoints with Supertest?

Yes, send POST requests with GraphQL queries. Supertest handles any HTTP method and body format, including GraphQL's { query: '...' } JSON payload.

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