Skip to content

Fix: Supertest Not Working — Requests Not Sending, Server Not Closing, or Assertions Failing

FixDevs ·

Quick Answer

How to fix Supertest HTTP testing issues — Express and Fastify setup, async test patterns, authentication headers, file uploads, JSON body assertions, and Vitest/Jest integration.

The Problem

Supertest requests hang and tests time out:

import request from 'supertest';
import app from './app';

test('GET /api/users', async () => {
  const res = await request(app).get('/api/users');
  // Test hangs — never resolves
});

Or the server port is already in use:

Error: listen EADDRINUSE: address already in use :::3000

Or assertions pass locally but fail in CI:

Expected status 200, received 500

Why This Happens

Supertest creates an HTTP server from your Express app and sends requests to it. Common issues:

  • The app must not call listen() — Supertest binds the app to a random ephemeral port internally. If your app already calls app.listen(3000), you get EADDRINUSE. Export the Express app separately from the server start.
  • Async middleware or database connections delay responses — if your app connects to a database on startup and the connection isn’t established when tests run, requests hang or return 500.
  • The server isn’t closed between tests — Supertest creates a new server for each request(app) call. Without proper cleanup, servers accumulate and ports get exhausted.
  • Request bodies need the right Content-Type.send({ name: 'Alice' }) sets Content-Type: application/json automatically, but .send('raw string') doesn’t. If your Express app expects JSON but receives text, parsing fails.

Fix 1: Separate App from Server

// src/app.ts — export the Express app (no listen)
import express from 'express';

const app = express();
app.use(express.json());

app.get('/api/users', async (req, res) => {
  const users = await db.query.users.findMany();
  res.json(users);
});

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  const user = await db.insert(users).values({ name, email }).returning();
  res.status(201).json(user[0]);
});

export default app;
// src/server.ts — start the server (not imported in tests)
import app from './app';

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
// tests/api.test.ts — test using the app, not the server
import request from 'supertest';
import app from '../src/app';

describe('Users API', () => {
  test('GET /api/users returns user list', async () => {
    const res = await request(app)
      .get('/api/users')
      .expect('Content-Type', /json/)
      .expect(200);

    expect(res.body).toBeInstanceOf(Array);
  });

  test('POST /api/users creates a user', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Alice', email: '[email protected]' })
      .expect(201);

    expect(res.body).toMatchObject({
      name: 'Alice',
      email: '[email protected]',
    });
    expect(res.body.id).toBeDefined();
  });

  test('POST /api/users validates input', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: '' })  // Missing email
      .expect(400);

    expect(res.body.error).toBe('Name and email required');
  });
});

Fix 2: Authentication Testing

import request from 'supertest';
import app from '../src/app';

describe('Protected Routes', () => {
  let authToken: string;

  beforeAll(async () => {
    // Login to get a token
    const res = await request(app)
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'password123' });

    authToken = res.body.token;
  });

  test('GET /api/profile returns user profile', async () => {
    const res = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(res.body.email).toBe('[email protected]');
  });

  test('GET /api/profile rejects without token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
  });

  // Custom headers
  test('API key authentication', async () => {
    await request(app)
      .get('/api/data')
      .set('X-API-Key', 'test-api-key')
      .expect(200);
  });

  // Cookie-based auth
  test('Cookie authentication', async () => {
    const loginRes = await request(app)
      .post('/api/auth/login')
      .send({ email: '[email protected]', password: 'pass' });

    // Extract cookie from login response
    const cookie = loginRes.headers['set-cookie'];

    await request(app)
      .get('/api/profile')
      .set('Cookie', cookie)
      .expect(200);
  });
});

Fix 3: Request Types

import request from 'supertest';
import app from '../src/app';
import path from 'path';

// JSON body
test('POST JSON', async () => {
  await request(app)
    .post('/api/posts')
    .send({ title: 'Hello', body: 'World' })  // Auto sets Content-Type: application/json
    .expect(201);
});

// Form data
test('POST form data', async () => {
  await request(app)
    .post('/api/contact')
    .type('form')
    .send('name=Alice&message=Hello')
    .expect(200);
});

// File upload (multipart)
test('POST file upload', async () => {
  const res = await request(app)
    .post('/api/upload')
    .attach('file', path.join(__dirname, 'fixtures/test-image.jpg'))
    .field('description', 'Test image')
    .expect(201);

  expect(res.body.filename).toMatch(/\.jpg$/);
  expect(res.body.size).toBeGreaterThan(0);
});

// Multiple files
test('POST multiple files', async () => {
  await request(app)
    .post('/api/upload/batch')
    .attach('files', 'tests/fixtures/file1.pdf')
    .attach('files', 'tests/fixtures/file2.pdf')
    .expect(201);
});

// Query parameters
test('GET with query params', async () => {
  const res = await request(app)
    .get('/api/search')
    .query({ q: 'typescript', page: 1, limit: 10 })
    .expect(200);

  expect(res.body.results).toHaveLength(10);
});

// Custom Content-Type
test('POST XML', async () => {
  await request(app)
    .post('/api/webhook')
    .set('Content-Type', 'application/xml')
    .send('<event><type>order.created</type></event>')
    .expect(200);
});

Fix 4: Response Assertions

import request from 'supertest';
import app from '../src/app';

test('detailed response assertions', async () => {
  const res = await request(app)
    .get('/api/users/123')
    .expect(200)
    .expect('Content-Type', /json/)          // Regex match on header
    .expect('Cache-Control', 'no-store')      // Exact header match
    .expect(res => {
      // Custom assertion function
      if (!res.body.id) throw new Error('Missing id');
      if (res.body.role !== 'admin') throw new Error('Expected admin role');
    });

  // Additional assertions with your test framework
  expect(res.body).toEqual({
    id: '123',
    name: expect.any(String),
    email: expect.stringContaining('@'),
    role: 'admin',
    createdAt: expect.any(String),
  });

  // Check response headers
  expect(res.headers['x-request-id']).toBeDefined();

  // Check status text
  expect(res.status).toBe(200);
  expect(res.ok).toBe(true);
});

// Check for specific error shapes
test('validation error response', async () => {
  const res = await request(app)
    .post('/api/users')
    .send({ name: '' })
    .expect(400);

  expect(res.body).toMatchObject({
    error: 'Validation failed',
    details: expect.arrayContaining([
      expect.objectContaining({ field: 'name', message: expect.any(String) }),
      expect.objectContaining({ field: 'email' }),
    ]),
  });
});

// Redirect assertion
test('redirect to login', async () => {
  await request(app)
    .get('/dashboard')
    .expect(302)
    .expect('Location', '/login');
});

Fix 5: Database Setup and Teardown

import request from 'supertest';
import app from '../src/app';
import { db } from '../src/db';
import { users } from '../src/schema';

beforeEach(async () => {
  // Clean database before each test
  await db.delete(users);

  // Seed test data
  await db.insert(users).values([
    { id: '1', name: 'Alice', email: '[email protected]', role: 'admin' },
    { id: '2', name: 'Bob', email: '[email protected]', role: 'user' },
  ]);
});

afterAll(async () => {
  // Close database connection
  await db.$client.end();
});

test('GET /api/users returns seeded data', async () => {
  const res = await request(app).get('/api/users').expect(200);
  expect(res.body).toHaveLength(2);
  expect(res.body[0].name).toBe('Alice');
});

test('DELETE /api/users/:id removes user', async () => {
  await request(app).delete('/api/users/1').expect(204);

  const res = await request(app).get('/api/users').expect(200);
  expect(res.body).toHaveLength(1);
  expect(res.body[0].name).toBe('Bob');
});

Fix 6: Fastify and Hono Testing

// Fastify — use inject instead of Supertest
import { build } from '../src/app';

test('GET /api/users', async () => {
  const app = await build();

  const res = await app.inject({
    method: 'GET',
    url: '/api/users',
    headers: { authorization: 'Bearer token' },
  });

  expect(res.statusCode).toBe(200);
  expect(JSON.parse(res.payload)).toHaveLength(2);
});

// Hono — use app.request()
import app from '../src/app';

test('GET /api/users', async () => {
  const res = await app.request('/api/users');
  expect(res.status).toBe(200);

  const body = await res.json();
  expect(body).toHaveLength(2);
});

// Hono with body
test('POST /api/users', async () => {
  const res = await app.request('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' }),
  });
  expect(res.status).toBe(201);
});

Still Not Working?

Tests hang and time out — the app calls app.listen() in the module you’re importing. Separate the Express app creation from listen(). Export app from one file and call listen() in a different entry point that tests don’t import.

EADDRINUSE error — a previous test left a server running. Use Supertest’s request(app) pattern (passes the app, not a URL). This creates an ephemeral server per request that auto-closes. Don’t use request('http://localhost:3000').

Response body is empty — your route handler might not be sending a response. Check for missing res.json() or res.send(). Also check that async errors are caught — an unhandled promise rejection in Express may cause the request to hang instead of returning 500.

Tests pass locally, fail in CI — usually a database issue. CI doesn’t have your local database. Use a test database (Docker PostgreSQL in CI) or mock the database layer. Also check for time-dependent tests that rely on specific dates.

For related testing issues, see Fix: Vitest Setup Not Working and Fix: MSW Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles