Fix: Supertest Not Working — Requests Not Sending, Server Not Closing, or Assertions Failing
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 :::3000Or assertions pass locally but fail in CI:
Expected status 200, received 500Why 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 callsapp.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' })setsContent-Type: application/jsonautomatically, 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.
Fix: BullMQ Not Working — Jobs Not Processing, Workers Not Starting, or Redis Connection Failing
How to fix BullMQ issues — queue and worker setup, Redis connection, job scheduling, retry strategies, concurrency, rate limiting, event listeners, and dashboard monitoring.
Fix: GraphQL Yoga Not Working — Schema Errors, Resolvers Not Executing, or Subscriptions Failing
How to fix GraphQL Yoga issues — schema definition, resolver patterns, context and authentication, file uploads, subscriptions with SSE, error handling, and Next.js integration.