End-to-end Tests
Boot the real application inside a test and send it HTTP requests with supertest, asserting status codes, bodies and validation, the things a unit test cannot see.
What you'll learn
- Create an application from a compiled testing module with createNestApplication() and init(), configured as main.ts configures it
- Send requests with supertest and assert status, body and headers in its expect() forms
- Manage one application per file with beforeAll and afterAll, and know what an e2e test proves that a unit test cannot
The unit tests so far proved that classes do what their methods say. None of them proved that POST /cats exists, that a body is validated before the handler runs, that a missing cat is a 404 and not a 500, or that the guard is applied to the route it should be. Those facts live in decorators and in the pipeline Nest runs around a handler, and they are only true once an application is built. An end-to-end test builds one and talks to it the way a client would: over HTTP, asserting status codes and bodies. It is slower than a unit test and it proves more.
Building the application in a test
The testing module can produce a whole application:
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();
});
createNestApplication() is NestFactory.create() for a compiled module; init() wires the routes and runs the lifecycle hooks without listening on a port, since nothing will connect through a socket. Note the pipe. Whatever main.ts does to the application, the test has to do too, or the test exercises a different application from the one that ships. Teams that take this seriously move that setup into a function both call, configureApp(app), so it cannot drift.
One application per spec file is the rule: beforeAll builds it, afterAll closes it, and the tests share it. That makes the tests order-dependent within the file, which the docs accept and so does this lesson: a GET that expects the cat a POST created must come after it. Prefer tests that leave the application as they found it, or reset state in beforeEach, when the order starts to matter for the wrong reasons.
Sending requests
supertest sends requests to the application's server object and returns a chain:
it('POST /orders', () => {
return request(app.getHttpServer())
.post('/orders')
.set('x-user', 'ada')
.send({ item: 'book' })
.expect(201)
.expect({ id: 1, item: 'book' });
});
.get(), .post(), .put(), .patch(), .delete() start a request; .set() adds a header; .query() a query string; .send() a body, JSON when given an object. .expect() has four forms, applied in order: a status, expect(201); a body, deep-equal for JSON, expect({ id: 1 }); a header, expect('content-type', /json/); and a function that receives the response and may assert anything, expect((res) => { ... }). The chain is a promise: return it from the test, or await it and look at res.status, res.body, res.headers yourself. A failed expectation reads like expected 201 "Created", got 200 "OK".
What to assert
Assert what a client can observe: the status, the body, headers that matter. GET /cats/99 should be a 404 whose body says why; POST /cats with { name: '', age: -1 } should be a 400 whose message lists both rule failures, because that list is what a form will show. Do not assert internals, such as which service method ran; that is what the unit tests were for, and an e2e test that reaches into the container is a unit test with worse startup time.
Something slow or external at the bottom of the module, a database, a payment gateway, is where overrideProvider() comes back: import the real AppModule, swap the one provider that must not run, and everything above it is tested as shipped.
Your task
The application has a validated POST /cats, a GET /cats, and a GET /cats/:id with ParseIntPipe. Write test/cats.e2e-spec.ts:
- In
beforeAll, compile a testing module that importsAppModule, create the application, apply aValidationPipewithwhitelistasmain.tswould, and init it. Close it inafterAll. - Write the five tests it lists: creating a cat, rejecting an invalid body with both messages, listing, a
404with its message, and a400for a non-numeric id.
The bugs to catch are the ones only a request can see: a wrong status code, a cat returned but not stored, a 404 that became a 200, a pipe that is missing.
When it fails
request() needs the app's server:app.getHttpServer()was called beforeinit(), orappwas never assigned inbeforeAll.- The invalid body is accepted with 201: the
ValidationPipeis not applied in the test. The application under test is the one the test configures. expected 200 "OK", got 404 "Not Found"onGET /cats: the test order, or the module:AppModulemust importCatsModulefor the routes to exist.- The tests hang and time out: the request was neither returned nor awaited, or
beforeAllnever calledinit().
Remember
createNestApplication()from a compiled module, then everythingmain.tsdoes, theninit();close()inafterAll.request(app.getHttpServer())then a verb,.set,.send, and.expect()for status, body, header or a function; return or await the chain.- Assert what a client sees; leave internals to unit tests.
- One application per file, tests in a deliberate order,
overrideProvider()for the one thing that must not run.
Stuck? Show a hint
beforeAll: const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = moduleRef.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); await app.init(). afterAll: await app.close(). Tests return request(app.getHttpServer()).post('/cats').send({...}).expect(201).expect({...}); for the message list, await the request and expect(res.body.message).toEqual([...]).