NestJS course Β· Module 7: Testing
Unit Testing - testing the fort's individual parts
In this lesson6
The catapult will not fire, but the culprit is nowhere in sight. Maybe a rope snapped, maybe the release mechanism failed, maybe the counterweight was miscalculated. Checking the whole machine at once tells you only that something is wrong - not what.
The legion's mechanic proceeds differently: he takes the machine apart and examines each part separately. Does the rope hold the tension? Does the mechanism release when pulled? A unit test does exactly the same with code - it checks the smallest element in isolation from the rest of the system.
The three words a test is built from
Let's start with the simplest case - a class with no dependencies at all:
1describe('Bolt', () => {
2 it('calculates the projectile weight', () => {
3 const bolt = new Bolt('iron', 5);
4
5 expect(bolt.getWeight()).toBe(5);
6 });
7});Three words and you have a test. describe groups the tests concerning one element - it is a chapter heading. it describes one behaviour, and its name reads as a sentence: "it calculates the projectile weight". expect states an expectation and compares it with reality, here through the toBe matcher.
Note the name inside it: it says what the element should do, not how it is built. When such a test fails in a CI report, you read "calculates the projectile weight" and know at once what broke - without opening the file.
The problem: a service is not created with new
We created the Bolt class with a plain new. But a NestJS service has injected dependencies - a repository, configuration, other services. Writing new LegionsService() will end in an error, because the constructor expects something.
You could pass those dependencies by hand, but then you bypass the whole injection mechanism and the test stops resembling reality. So NestJS offers a testing module: a miniature version of the application containing only what one test needs.
The testing module's three steps
Building the module is always the same sequence:
1const module = await Test.createTestingModule({
2 providers: [
3 LegionsService,
4 { provide: getRepositoryToken(Legion), useValue: repoMock },
5 ],
6}).compile();
7
8const service = module.get<LegionsService>(LegionsService);Step one: Test.createTestingModule({...}) creates an isolated module for the test. In providers you list only what the test genuinely needs - the service under test and its dependencies. Here we swap the real repository for a double via useValue, so the test never touches a database.
Step two: .compile() finalises the configuration - NestJS resolves the dependencies between providers and creates their instances. It is an asynchronous operation, hence the await. Without this call you hold merely a description of a module, not a working one.
Step three: module.get(LegionsService) takes the provider instance out of the finished module - the very one NestJS built, with the repository double already injected. The <LegionsService> notation is a type parameter; thanks to it TypeScript knows what you are getting and will suggest its methods.
Remember the three as one: build, compile, take out. Skipping compile() is the most common mistake with a first testing module.
A fresh fort before every trial
The code above usually goes into a beforeEach hook:
1describe('LegionsService', () => {
2 let service: LegionsService;
3 let repoMock: { find: jest.Mock };
4
5 beforeEach(async () => {
6 repoMock = { find: jest.fn() };
7
8 const module = await Test.createTestingModule({
9 providers: [
10 LegionsService,
11 { provide: getRepositoryToken(Legion), useValue: repoMock },
12 ],
13 }).compile();
14
15 service = module.get<LegionsService>(LegionsService);
16 });
17});beforeEach runs before every test, so each one gets a fresh service instance and a clean double. Why, when building the module costs time?
Because without it the tests start affecting one another. A service that stored something in memory during one test carries that state into the next - and then the result begins to depend on the order of execution. Such a test can be green on your machine and red in CI, where Jest parallelises differently. A fresh instance removes that whole class of problems with one line.
One test, one behaviour
With the module in place, we write the actual tests:
1it('returns the list of legions', async () => {
2 repoMock.find.mockResolvedValue([{ id: 1, name: 'Legio X' }]);
3
4 const result = await service.findAll();
5
6 expect(result).toHaveLength(1);
7});
8
9it('returns an empty list when there are no legions', async () => {
10 repoMock.find.mockResolvedValue([]);
11
12 const result = await service.findAll();
13
14 expect(result).toEqual([]);
15});Two behaviours, two tests - and this is a rule I recommend holding to strictly. It is tempting to check both cases in one it, but then you lose the most valuable property of unit tests: when one fails, you know exactly what. A test examining five things at once says as much as a catapult that will not fire - something is wrong, go find it yourself.
Note also what these tests do not check: whether the repository really fetches data from a database. That is deliberate - the double cuts the database off, so we examine the service's logic alone. We will check real elements working together with a different kind of test.
Summary
The machine is apart and every part examined on its own:
- a unit test examines the smallest element in isolation from the rest of the system,
describegroups one element's tests,itdescribes one behaviour,expectstates an expectation,- the name inside
itsays what the element should do - it is what appears in the report after a failure, - a NestJS service is not created with
new, because it has injected dependencies - that is what the testing module is for, - three steps:
Test.createTestingModule({providers})builds an isolated module,.compile()finalises it and creates the instances,module.get(Service)takes out the ready provider, - skipping
.compile()is the most common mistake - without it you hold a description of a module, not a module, useValueswaps a real dependency for a double, so the test never touches a database,beforeEachgives a fresh instance before every test - without it the result starts depending on execution order,- one behaviour per test: a test examining five things does not tell you which one broke.
In the next lesson we will check what a unit test by design cannot see - whether the elements really work together. For now remember: build, compile, take out - and then examine one part at a time.
Code for this lesson: src/unit-testing.spec.ts
1// Unit Testing - Testowanie Poszczegolnych Elementow Fortu
2import { Test, TestingModule } from '@nestjs/testing';
3
4// Service for testing - province management
5class ProvinceService {
6 private provinces = [
7 { id: 1, name: 'Gallia', governor: 'Caesar', tribute: 1000 },
8 { id: 2, name: 'Aegyptus', governor: 'Cleopatra', tribute: 2000 },
9 { id: 3, name: 'Britannia', governor: 'Agricola', tribute: 500 },
10 ];
11
12 findAll() {
13 return this.provinces;
14 }
15
16 findOne(id: number) {
17 return this.provinces.find(p => p.id === id) || null;
18 }
19
20 calculateTotalTribute(): number {
21 return this.provinces.reduce((sum, p) => sum + p.tribute, 0);
22 }
23
24 getProvincesByGovernor(governor: string) {
25 return this.provinces.filter(p => p.governor === governor);
26 }
27}
28
29describe('ProvinceService - Unit Tests', () => {
30 let service: ProvinceService;
31
32 // beforeEach - preparation before each test
33 beforeEach(() => {
34 service = new ProvinceService();
35 });
36
37 describe('findAll', () => {
38 it('should return all provinces', () => {
39 const provinces = service.findAll();
40 expect(provinces).toHaveLength(3);
41 expect(provinces[0].name).toBe('Gallia');
42 });
43 });
44
45 describe('findOne', () => {
46 it('should return a province by id', () => {
47 const province = service.findOne(1);
48 expect(province).toBeDefined();
49 expect(province!.name).toBe('Gallia');
50 });
51
52 it('should return null for non-existent id', () => {
53 const province = service.findOne(999);
54 expect(province).toBeNull();
55 });
56 });
57
58 describe('calculateTotalTribute', () => {
59 it('should calculate total tribute correctly', () => {
60 const total = service.calculateTotalTribute();
61 expect(total).toBe(3500); // 1000 + 2000 + 500
62 });
63 });
64
65 describe('getProvincesByGovernor', () => {
66 it('should filter provinces by governor', () => {
67 const provinces = service.getProvincesByGovernor('Caesar');
68 expect(provinces).toHaveLength(1);
69 expect(provinces[0].name).toBe('Gallia');
70 });
71
72 it('should return empty array for unknown governor', () => {
73 const provinces = service.getProvincesByGovernor('Nero');
74 expect(provinces).toHaveLength(0);
75 });
76 });
77});
78Spotted 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. Test.createTestingModule() in NestJS is used for:
2. What does the compile() method do in TestingModule?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a TestingModule configuration with injected dependencies
- Horizontal ordering
Arrange the TestingModule creation in the correct order: