We use cookies to enhance your experience on the site
CodeWorlds

Testing - Trials Before Setting Out on a Campaign

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.

Why Is Testing Critical?

Imagine you are a centurion responsible for the safety of the entire legion. Before setting out on a campaign, you must check:

  • Whether the ballistae work - unit tests
  • Whether the legion cooperates - integration tests
  • Whether the entire fort is ready for the expedition - end-to-end tests
  • How the fort handles a storm - stress tests
  • Whether all systems work reliably - reliability tests
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});

Types of Tests in a Roman Fort

1. Unit Tests - Testing Individual Components

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});

2. Integration Tests - Testing Cooperation

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});

3. End-to-End Tests - Full Expedition Simulation

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});

Testing Framework in NestJS

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}

Basic Testing Concepts

Test Doubles - Stand-ins in the Application

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};

Test Lifecycle

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});

Testing in NestJS - Special Tools

Test Module Builder

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});

Best Testing Practices

1. AAA Pattern - Arrange, Act, Assert

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});

2. Test Isolation

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});

3. Descriptive Test Names

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});

Example of a Complete Service Test

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.

Go to CodeWorlds