NestJS course Β· Module 7: Testing
Integration Testing - checking that the legion works together
In this lesson6
Every part of the catapult passed inspection. The rope holds, the mechanism releases, the counterweight weighs exactly what it should. You assemble the machine - and the projectile flies three paces. Why? Because the rope turned out to be a metre too short for that arm. Every part was sound on its own, but nobody checked whether they fit together.
That is the gap a unit test cannot see by design - in it we replaced every neighbouring part with a double. An integration test fills the gap: it assembles several real elements and checks whether they talk to each other the way we assumed.
The testing pyramid - how much of what to write
Before we write such a test, let's place it among the others. Tests form a pyramid - the most numerous at the bottom, the rarest at the top:
- Unit - the base. The most numerous and fastest, because they touch nothing beyond one class.
- Integration - the middle layer. Fewer and slower, because they run several elements at once.
- E2E - the peak. The fewest and slowest; they travel a request's whole road.
- Manual tests - occasional, where automation cannot reach.
The pyramid's shape is not arbitrary: the higher you go, the more confidence a test gives, but the more it costs in time and the more readily it breaks for trivial reasons. An inverted pyramid - a handful of unit tests and hundreds of E2E ones - gives you a suite that runs for a quarter of an hour and fails whenever a button's label changes.
The same order governs running them: npm run test (unit, seconds) β test:watch (the same, looping while you write) β test:cov (unit plus coverage counting) β test:e2e (the slowest, last).
Unit versus integration - one difference
The distinction comes down to a single question: how many real elements take part in the test?
A unit test examines one component and replaces all its dependencies with doubles. An integration test examines several working together - a controller with its service, a service with its repository.
That does not mean doubles disappear in integration tests. You still swap out whatever lies beyond the boundary of the slice under test: payments, email delivery, somebody else's API. You are only moving the boundary - from one class to several cooperating ones.
Assembling a module from real parts
An integration test builds a module much like a unit test, but instead of listing individual providers it imports the whole application module:
1const module = await Test.createTestingModule({
2 imports: [
3 TypeOrmModule.forRoot({
4 type: 'sqlite',
5 database: ':memory:',
6 entities: [Legion],
7 synchronize: true,
8 }),
9 LegionsModule,
10 ],
11}).compile();LegionsModule comes in whole - with its controller, service and repository, exactly as in the real application. The new part is the database configuration: type: 'sqlite' with database: ':memory:' creates a database existing only in the process's memory. It appears when the test starts and vanishes when it ends, so you need no server at all, and every run begins with an empty table.
synchronize: true tells TypeORM to build the tables straight from the entities. In a production application that option is forbidden - migrations rule there - but in a database that lives three seconds it is exactly what you want.
Testing through real HTTP
Since the module contains a controller, we can knock on it the way a client would - with an HTTP request:
1describe('LegionsController (integration)', () => {
2 let app: INestApplication;
3
4 beforeAll(async () => {
5 const module = await Test.createTestingModule({
6 imports: [/* ... */],
7 }).compile();
8
9 app = module.createNestApplication();
10 await app.init();
11 });
12
13 afterAll(async () => {
14 await app.close();
15 });
16
17 it('POST /legions saves a legion and returns it with an id', async () => {
18 const response = await request(app.getHttpServer())
19 .post('/legions')
20 .send({ name: 'Legio X' })
21 .expect(201);
22
23 expect(response.body.id).toBeDefined();
24 expect(response.body.name).toBe('Legio X');
25 });
26});Three new elements are worth naming. module.createNestApplication() turns the compiled module into a running application - one able to handle a request. app.init() starts it; without that call the server will accept nothing.
Then request(app.getHttpServer()) - this is the supertest library. getHttpServer() pulls the HTTP server out of the application, and request(...) sends it a real request without opening a port. The traffic happens in memory, so the test occupies no network and will not collide with another process.
The chain reads like a description of the request: .post('/legions') picks the method and address, .send({...}) adds the body, .expect(201) checks the response code. The returned response.body we then examine with ordinary assertions.
And here you see what this test gives beyond a unit one: the projectile travelled the whole road - through routing, body validation, the service, the repository, into the database and back. Had the controller expected a title field while the service saved name, unit tests of both classes would pass without blinking; this one fails.
Cleaning up between tests
A real database, even one in memory, remembers everything written to it - including from the previous test:
1afterEach(async () => {
2 await app.get(getRepositoryToken(Legion)).clear();
3});clear() empties the table after each test. Without it, a test counting legions will also see those created earlier and will start depending on execution order - exactly the trap beforeEach defused in unit tests.
Note the choice of hooks: we build the application in beforeAll (once, because it is expensive) and clear the data in afterEach (every time, because it is cheap). Closing with app.close() in afterAll is mandatory - otherwise Jest hangs with an open database connection.
Summary
The machine is assembled and fired - for real this time:
- an integration test checks several components working together, where a unit test examines one in isolation,
- doubles do not disappear - only the boundary you place them at moves,
- the testing pyramid: most unit tests, fewer integration, fewest E2E, occasional manual,
- the order from fastest:
test,test:watch,test:cov,test:e2e, - we assemble the module from real parts with
imports: [Module], replacing the database with sqlite:memory:andsynchronize: true, createNestApplication()turns a module into a running application,app.init()starts it,request(app.getHttpServer())from the supertest library sends a real request without opening a port,- the chain
.post().send().expect(201)describes the request, and you examineresponse.bodywith ordinary assertions, clear()inafterEachempties the table,app.close()inafterAllcloses the application - without it Jest hangs.
In the next lesson we will climb to the pyramid's very peak - to E2E tests, which travel a user's full road. For now remember: a unit test says every part works; an integration test says they fit together.
Code for this lesson: src/integration-testing.spec.ts
1// Integration Testing - Checking Legion Cooperation
2import { Test, TestingModule } from '@nestjs/testing';
3import { Injectable } from '@nestjs/common';
4
5// Supply service
6@Injectable()
7class SupplyService {
8 calculateSupplies(soldiers: number): number {
9 // Each soldier needs 3 units of supplies
10 return soldiers * 3;
11 }
12
13 checkSupplyStatus(current: number, required: number) {
14 return {
15 sufficient: current >= required,
16 shortage: Math.max(0, required - current),
17 };
18 }
19}
20
21// Garrison service - depends on SupplyService
22@Injectable()
23class GarrisonService {
24 constructor(private supplyService: SupplyService) {}
25
26 assessReadiness(soldiers: number, currentSupplies: number) {
27 const requiredSupplies = this.supplyService.calculateSupplies(soldiers);
28 const status = this.supplyService.checkSupplyStatus(
29 currentSupplies, requiredSupplies
30 );
31
32 return {
33 soldiers,
34 requiredSupplies,
35 currentSupplies,
36 ready: status.sufficient,
37 shortage: status.shortage,
38 };
39 }
40}
41
42// Integration test - checks how the services work together
43describe('Garrison Integration Test', () => {
44 let garrisonService: GarrisonService;
45 let supplyService: SupplyService;
46
47 beforeEach(async () => {
48 // We create a REAL module with both services
49 const module: TestingModule = await Test.createTestingModule({
50 providers: [GarrisonService, SupplyService],
51 }).compile();
52
53 garrisonService = module.get(GarrisonService);
54 supplyService = module.get(SupplyService);
55 });
56
57 it('should assess garrison as ready when supplies sufficient', () => {
58 const result = garrisonService.assessReadiness(100, 400);
59
60 expect(result.ready).toBe(true);
61 expect(result.shortage).toBe(0);
62 expect(result.requiredSupplies).toBe(300);
63 });
64
65 it('should detect supply shortage', () => {
66 const result = garrisonService.assessReadiness(100, 200);
67
68 expect(result.ready).toBe(false);
69 expect(result.shortage).toBe(100); // 300 - 200
70 });
71
72 it('should correctly integrate supply calculation', () => {
73 // We check the data flow between the services
74 const supplies = supplyService.calculateSupplies(50);
75 expect(supplies).toBe(150);
76
77 const readiness = garrisonService.assessReadiness(50, supplies);
78 expect(readiness.ready).toBe(true);
79 });
80});
81Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Integration tests in NestJS verify:
2. The main difference between unit and integration tests is:
Hands-on tasks in the game
- Code editor
Create an integration test for the legions module
- Vertical ordering
Arrange the testing pyramid from most numerous (bottom) to least numerous (top):
- Click in order
Arrange the test execution commands from fastest: