Skip to content

Testing Express with Supertest

Supertest is the standard library for sending HTTP-like requests directly to an Express app object in tests, without binding to a real network port. This lesson covers its API in depth.

How Supertest Works

request(app) wraps your Express app and returns a chainable object mirroring the HTTP methods, .get(), .post(), .put(), .delete(). Each call returns a promise resolving to a response object with status, body, headers, and more.

Because it operates directly on the app object rather than a live server, tests run fast and don't require managing port conflicts or actually starting the process.

const request = require('supertest');
const app = require('../app');

test('GET /users returns an array', async () => {
  const res = await request(app).get('/users');

  expect(res.status).toBe(200);
  expect(Array.isArray(res.body)).toBe(true);
});

await request(app).get('/users') sends the request and resolves once the response is fully received.

Chaining Requests With Data

await request(app)
  .post('/users')
  .send({ email: 'ada@example.com' })
  .set('Content-Type', 'application/json')
  .expect(201);
  • .send(data) attaches a request body, automatically serialized as JSON for object payloads.
  • .set(header, value) attaches custom headers, like Authorization.
  • .expect(status) asserts the status code inline and throws if it doesn't match.
  • .expect(status, body) can assert both status and exact body in one call.

Supertest Cheatsheet

The methods and assertions you will use constantly with supertest.

Task Code
GET request request(app).get('/users')
POST with JSON body request(app).post('/users').send({ email })
Custom header .set('Authorization', 'Bearer token')
Inline status assertion .expect(201)
File upload field .attach('avatar', 'test/fixtures/photo.jpg')
Query string request(app).get('/users').query({ page: 2 })

Testing File Uploads

For routes using multer, .attach() lets a test simulate a real multipart file upload, pointing at a small fixture file included in the test suite.

test('uploads an avatar', async () => {
  const res = await request(app)
    .post('/upload')
    .attach('avatar', 'test/fixtures/avatar.png');

  expect(res.status).toBe(200);
  expect(res.body.filename).toBeDefined();
});

Chaining Multiple Requests in One Flow

Some test scenarios naturally involve a sequence of requests, create a resource, then fetch it, then delete it, and confirm it's gone.

test('full lifecycle of a task', async () => {
  const create = await request(app).post('/tasks').send({ title: 'Write tests' });
  const id = create.body.id;

  const get = await request(app).get(`/tasks/${id}`);
  expect(get.status).toBe(200);

  await request(app).delete(`/tasks/${id}`).expect(204);

  const afterDelete = await request(app).get(`/tasks/${id}`);
  expect(afterDelete.status).toBe(404);
});

Common Mistakes

  • Forgetting await before a supertest request, causing assertions to run against a pending promise.
  • Passing a running server (via app.listen()) instead of the plain app object, opening unnecessary real network ports during tests.
  • Not cleaning up resources created during a test, letting them leak into other tests.
  • Asserting only on status codes and never checking the actual response body shape.

Key Takeaways

  • Supertest sends requests directly to an Express app object, no real network port required.
  • .send(), .set(), and .expect() chain together to build a complete request assertion.
  • .attach() supports testing multipart file upload routes.
  • Multi-step test flows can chain several requests to verify a full resource lifecycle.

Pro Tip

Pass the plain exported app object into request(), never a server returned by app.listen(), supertest manages its own ephemeral port internally and doesn't need (or want) a real listener.