Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

End-to-end tests with supertest

An end-to-end (e2e) test checks the application the way a client sees it: it sends real HTTP requests and looks at the status codes and response bodies. Unlike a unit test, all parts work together here — pipes, guards, services and the database. A NestJS project already contains such a test and the supertest library for sending requests.

Where the tests live

End-to-end tests live in the test/ folder in the project root, in files named *.e2e-spec.ts. An example is test/app.e2e-spec.ts. They run with a separate command:

npm run test:e2e

A separate database for tests

Tests create and delete data, so they need their own database. SQLite can keep a database in memory: if the path is :memory:, the database is created at startup and disappears afterwards. Environment variables take priority over the .env file, so it is enough to set DB_PATH at the top of the test.

A posts test

Create test/posts.e2e-spec.ts:

// test/posts.e2e-spec.ts
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';
 
process.env.DB_PATH = ':memory:';
 
describe('Posts (e2e)', () => {
  let app: INestApplication;
 
  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
 
    app = moduleRef.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });
 
  afterAll(async () => {
    await app.close();
  });
 
  it('GET /posts returns an empty list', () => {
    return request(app.getHttpServer()).get('/posts').expect(200).expect([]);
  });
 
  it('GET /posts/999 returns 404', () => {
    return request(app.getHttpServer()).get('/posts/999').expect(404);
  });
});

createNestApplication() creates the application from the whole AppModule but does not start it on a port: supertest passes requests to the server itself through app.getHttpServer(). Global settings from src/main.ts do not reach the test, so ValidationPipe is applied here again.

Requests with a body and a token

// test/posts.e2e-spec.ts
it('POST /posts without a token returns 401', () => {
  return request(app.getHttpServer())
    .post('/posts')
    .send({ title: 'Test', content: 'Text' })
    .expect(401);
});
 
it('logs in and creates a post', async () => {
  await request(app.getHttpServer())
    .post('/users')
    .send({ email: 'anna@example.com', name: 'Anna', password: 'password123' })
    .expect(201);
 
  const login = await request(app.getHttpServer())
    .post('/auth/login')
    .send({ email: 'anna@example.com', password: 'password123' })
    .expect(200);
 
  const response = await request(app.getHttpServer())
    .post('/posts')
    .set('Authorization', `Bearer ${login.body.accessToken}`)
    .send({ title: 'Test', content: 'Text' })
    .expect(201);
 
  expect(response.body.id).toBeDefined();
});

send() sends JSON and sets the Content-Type header itself, set() adds a header, and response.body holds the parsed response. On success the terminal shows Tests: 4 passed, 4 total.

Common mistake

Tests run against the working blog.sqlite database and spoil or delete real data. Always set a separate database for tests before the module is created: the line process.env.DB_PATH = ':memory:' must come before Test.createTestingModule(). For PostgreSQL create a separate test database and point to it with environment variables.

Thanks for reading!

Donate to the site — we work hard for you.