Testers of the legions! Consul Caesar.js knows well that no wise legionary sets out on a campaign without thoroughly checking every element of the fort. Today you will learn the art of testing - the most important skill of every professional legionary-programmer!
In a Roman fort there is no room for errors. If a catapult fails to fire at a crucial moment, if the walls don't withstand a siege, or if the defense system fails during an attack - it means disaster! That's why every element must be tested before we set out on a campaign.
Imagine you are a centurion responsible for the safety of the entire legion. Before setting out on a campaign, you must check:
1// Basic testing philosophy in a Roman fort
2describe('application', () => {
3 it('should be ready to march', () => {
4 const cohort = new LegionaryCohort();
5 expect(cohort.isCombatReady()).toBe(true);
6 });
7
8 it('should survive a storm', () => {
9 const cohort = new LegionaryCohort();
10 const storm = new Storm({ intensity: 'high' });
11
12 expect(() => cohort.weatherStorm(storm)).not.toThrow();
13 expect(cohort.status).toBe('operational');
14 });
15});It's like checking each ballista separately - whether it loads properly, fires in the right direction, and doesn't explode:
1// ballista.spec.ts
2describe('Ballista', () => {
3 let ballista: Ballista;
4
5 beforeEach(() => {
6 ballista = new Ballista('Front Line Ballista #1');
7 });
8
9 it('should load with ammunition', () => {
10 ballista.load(BoltType.IRON);
11 expect(ballista.isLoaded()).toBe(true);
12 expect(ballista.getAmmunitionType()).toBe(BoltType.IRON);
13 });
14
15 it('should fire when loaded', () => {
16 ballista.load(BoltType.IRON);
17 const result = ballista.fire();
18
19 expect(result.hit).toBeDefined();
20 expect(ballista.isLoaded()).toBe(false);
21 });
22
23 it('should not fire when not loaded', () => {
24 expect(() => ballista.fire()).toThrow('Ballista not loaded!');
25 });
26});It's like combat exercises for the entire legion - whether the ballistae cooperate with the navigation system, whether communication between the front lines works:
1// cohort-combat.integration.spec.ts
2describe('Legion Combat Integration', () => {
3 let cohort: LegionaryCohort;
4 let enemy: EnemyLegion;
5
6 beforeEach(() => {
7 cohort = new LegionaryCohort();
8 cohort.addBallista(new Ballista('Left Flank #1'));
9 cohort.addBallista(new Ballista('Right Flank #1'));
10 enemy = new EnemyLegion();
11 });
12
13 it('should coordinate attack of all ballistae', async () => {
14 const battleResult = await cohort.attackEnemy(enemy);
15
16 expect(battleResult.shotsfired).toBe(2);
17 expect(battleResult.hits).toBeGreaterThan(0);
18 expect(enemy.health).toBeLessThan(enemy.maxHealth);
19 });
20});It's like a complete campaign test - from marching out of the fort, through fighting enemies, to returning with tributes:
1// tribute-hunt.e2e.spec.ts
2describe('Complete Tribute Hunt', () => {
3 let app: INestApplication;
4
5 beforeEach(async () => {
6 const moduleFixture = await Test.createTestingModule({
7 imports: [AppModule],
8 }).compile();
9
10 app = moduleFixture.createNestApplication();
11 await app.init();
12 });
13
14 it('should conduct a complete tribute expedition', async () => {
15 // 1. Plan the expedition
16 const expeditionPlan = await request(app.getHttpServer())
17 .post('/expeditions/plan')
18 .send({
19 destination: 'Tribute Island',
20 legionSize: 20,
21 duration: 7
22 })
23 .expect(201);
24
25 // 2. Execute the expedition
26 const expedition = await request(app.getHttpServer())
27 .post('/expeditions/execute')
28 .send({ planId: expeditionPlan.body.id })
29 .expect(201);
30
31 // 3. Check results
32 expect(expedition.body.success).toBe(true);
33 expect(expedition.body.tributesFound).toBeGreaterThan(0);
34 expect(expedition.body.legionStatus).toBe('healthy');
35 });
36});NestJS uses Jest by default - a powerful testing framework that supports all types of tests:
1// package.json
2{
3 "scripts": {
4 "test": "jest",
5 "test:watch": "jest --watch",
6 "test:cov": "jest --coverage",
7 "test:e2e": "jest --config ./test/jest-e2e.json"
8 },
9 "jest": {
10 "moduleFileExtensions": ["js", "json", "ts"],
11 "rootDir": "src",
12 "testRegex": ".*\.spec\.ts$",
13 "transform": {
14 "^.+\.(t|j)s$": "ts-jest"
15 },
16 "collectCoverageFrom": [
17 "**/*.(t|j)s"
18 ],
19 "coverageDirectory": "../coverage",
20 "testEnvironment": "node"
21 }
22}Sometimes we need to replace real components with their imitations:
1// Mock - imitation of external services
2const mockWeatherService = {
3 getWeather: jest.fn().mockResolvedValue({
4 condition: 'sunny',
5 windSpeed: 15,
6 temperature: 25
7 })
8};
9
10// Spy - spying on method calls
11const spyOnBallistaFire = jest.spyOn(ballista, 'fire');
12
13// Stub - fixed responses
14const tributeServiceStub = {
15 findTribute: () => Promise.resolve(new Tribute('Golden Coin'))
16};1describe('Legionary Cohort Management', () => {
2 let legion: LegionaryCohort;
3
4 // Before all tests
5 beforeAll(async () => {
6 // Global configuration
7 await setupTestDatabase();
8 });
9
10 // Before each test
11 beforeEach(() => {
12 legion = new LegionaryCohort();
13 legion.addLegionary(new Legionary('Marcus Aquila', 'Centurion'));
14 });
15
16 // After each test
17 afterEach(() => {
18 legion.dismiss();
19 });
20
21 // After all tests
22 afterAll(async () => {
23 await cleanupTestDatabase();
24 });
25
26 it('should manage the cohort', () => {
27 expect(legion.size()).toBe(1);
28 expect(legion.getCenturion().name).toBe('Marcus Aquila');
29 });
30});1// legion.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3
4describe('LegionService', () => {
5 let service: LegionService;
6 let module: TestingModule;
7
8 beforeEach(async () => {
9 module = await Test.createTestingModule({
10 providers: [
11 LegionService,
12 {
13 provide: 'LegionaryRepository',
14 useValue: mockLegionaryRepository,
15 },
16 ],
17 }).compile();
18
19 service = module.get<LegionService>(LegionService);
20 });
21
22 afterEach(async () => {
23 await module.close();
24 });
25
26 it('should recruit a new legionary', async () => {
27 const legionariesData = { name: 'Aurelia Bona', rank: 'Speculator' };
28 const result = await service.recruitLegionary(legionariesData);
29
30 expect(result.name).toBe('Aurelia Bona');
31 expect(mockLegionaryRepository.save).toHaveBeenCalledWith(legionariesData);
32 });
33});1it('should calculate tribute value with tax', () => {
2 // Arrange - preparation
3 const tribute = new Tribute('Golden Chalice', 1000);
4 const taxCalculator = new TaxCalculator();
5
6 // Act - execution
7 const totalValue = taxCalculator.calculateWithTax(tribute, 0.1);
8
9 // Assert - verification
10 expect(totalValue).toBe(1100);
11});1// ✅ Good test - independent
2it('should find tribute by ID', () => {
3 const tribute = tributeService.findById(1);
4 expect(tribute.name).toBe('Golden Compass');
5});
6
7// ❌ Bad test - dependent on other tests
8let globalTributeId;
9it('should add tribute', () => {
10 globalTributeId = tributeService.add(new Tribute('Ruby'));
11});
12it('should find added tribute', () => {
13 const tribute = tributeService.findById(globalTributeId);
14 expect(tribute).toBeDefined();
15});1// ✅ Good name
2it('should throw exception when legionary tries to pick up tribute heavier than their maximum weight', () => {
3 // test
4});
5
6// ❌ Weak name
7it('test lifting', () => {
8 // test
9});1// tribute.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { TributeService } from './tribute.service';
5import { Tribute } from './tribute.entity';
6import { Repository } from 'typeorm';
7
8describe('TributeService', () => {
9 let service: TributeService;
10 let repository: Repository<Tribute>;
11
12 const mockRepository = {
13 find: jest.fn(),
14 findOne: jest.fn(),
15 save: jest.fn(),
16 delete: jest.fn(),
17 create: jest.fn(),
18 };
19
20 beforeEach(async () => {
21 const module: TestingModule = await Test.createTestingModule({
22 providers: [
23 TributeService,
24 {
25 provide: getRepositoryToken(Tribute),
26 useValue: mockRepository,
27 },
28 ],
29 }).compile();
30
31 service = module.get<TributeService>(TributeService);
32 repository = module.get<Repository<Tribute>>(getRepositoryToken(Tribute));
33 });
34
35 afterEach(() => {
36 jest.clearAllMocks();
37 });
38
39 describe('findAll', () => {
40 it('should return an array of tributes', async () => {
41 const tributes = [
42 { id: 1, name: 'Golden Coin', value: 100 },
43 { id: 2, name: 'Silver Ring', value: 50 },
44 ];
45
46 mockRepository.find.mockResolvedValue(tributes);
47
48 const result = await service.findAll();
49
50 expect(result).toEqual(tributes);
51 expect(repository.find).toHaveBeenCalledTimes(1);
52 });
53
54 it('should return an empty array when no tributes exist', async () => {
55 mockRepository.find.mockResolvedValue([]);
56
57 const result = await service.findAll();
58
59 expect(result).toEqual([]);
60 expect(result.length).toBe(0);
61 });
62 });
63
64 describe('create', () => {
65 it('should create a new tribute', async () => {
66 const tributeData = { name: 'Diamond Ring', value: 5000 };
67 const expectedTribute = { id: 1, ...tributeData };
68
69 mockRepository.create.mockReturnValue(expectedTribute);
70 mockRepository.save.mockResolvedValue(expectedTribute);
71
72 const result = await service.create(tributeData);
73
74 expect(result).toEqual(expectedTribute);
75 expect(repository.create).toHaveBeenCalledWith(tributeData);
76 expect(repository.save).toHaveBeenCalledWith(expectedTribute);
77 });
78
79 it('should throw an exception for invalid data', async () => {
80 const invalidData = { name: '', value: -100 };
81 mockRepository.save.mockRejectedValue(new Error('Validation failed'));
82
83 await expect(service.create(invalidData)).rejects.toThrow('Validation failed');
84 });
85 });
86});Testing is not just about checking whether code works - it's a guarantee that your application will survive every storm and reach its destination with the entire legion and tributes! In the following modules, we'll learn the details of different types of tests.