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:e2eA 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.