Cours NestJS · Module 7: Tests
Les tests - les épreuves avant de partir en campagne
Dans cette leçon7
Testeurs des légions ! Le consul Caesar.js sait bien qu'aucun légionnaire avisé ne part en campagne sans vérifier soigneusement chaque élément du fort. Aujourd'hui, tu vas apprendre l'art des tests - la compétence la plus importante de tout légionnaire-programmeur professionnel !
Dans un fort romain, il n'y a pas de place pour les erreurs. Si une catapulte ne tire pas au moment crucial, si les murs ne résistent pas à un siège, ou si le système de défense faillit lors d'une attaque - c'est le désastre ! C'est pourquoi chaque élément doit être testé avant de partir en campagne.
Pourquoi les tests sont-ils cruciaux ?
Imagine que tu es un centurion responsable de la sécurité de toute la légion. Avant de partir en campagne, tu dois vérifier :
- Si les canons fonctionnent - les tests unitaires
- Si la légion coopère - les tests d'intégration
- Si le fort entier est prêt pour l'expédition - les tests end-to-end
- Comment le fort supporte une tempête - les tests de charge (stress)
- Si tous les systèmes fonctionnent de manière fiable - les tests de fiabilité
1// Basic testing philosophy in a Roman fort
2describe('application', () => {
3 it('should be ready to sail', () => {
4 const cohort = new LegionaryLegion();
5 expect(cohort.isSeaworthy()).toBe(true);
6 });
7
8 it('should survive a storm', () => {
9 const cohort = new LegionaryLegion();
10 const storm = new Storm({ intensity: 'high' });
11
12 expect(() => cohort.weatherStorm(storm)).not.toThrow();
13 expect(cohort.status).toBe('operational');
14 });
15});Types de tests dans un fort romain
1. Tests unitaires - tester les composants individuels
C'est comme vérifier chaque canon séparément - s'il se charge correctement, tire dans la bonne direction et n'explose pas :
1// cannon.spec.ts
2describe('Cannon', () => {
3 let cannon: Cannon;
4
5 beforeEach(() => {
6 cannon = new Cannon('Main Deck Cannon #1');
7 });
8
9 it('should load with ammunition', () => {
10 cannon.load(CannonballType.IRON);
11 expect(cannon.isLoaded()).toBe(true);
12 expect(cannon.getAmmunitionType()).toBe(CannonballType.IRON);
13 });
14
15 it('should fire when loaded', () => {
16 cannon.load(CannonballType.IRON);
17 const result = cannon.fire();
18
19 expect(result.hit).toBeDefined();
20 expect(cannon.isLoaded()).toBe(false);
21 });
22
23 it('should not fire when not loaded', () => {
24 expect(() => cannon.fire()).toThrow('Cannon not loaded!');
25 });
26});2. Tests d'intégration - tester la coopération
C'est comme des exercices de combat pour toute la légion - si les canons coopèrent avec le système de navigation, si la communication entre les ponts fonctionne :
1// cohort-combat.integration.spec.ts
2describe('Legion Combat Integration', () => {
3 let cohort: LegionaryLegion;
4 let enemy: EnemyLegion;
5
6 beforeEach(() => {
7 cohort = new LegionaryLegion();
8 cohort.addCannon(new Cannon('Port #1'));
9 cohort.addCannon(new Cannon('Starboard #1'));
10 enemy = new EnemyLegion();
11 });
12
13 it('should coordinate attack of all cannons', 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. Tests end-to-end - simulation complète de l'expédition
C'est comme un test de voyage complet - du départ du port, en passant par les combats contre les ennemis, jusqu'au retour avec les tributs :
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});Le framework de test dans NestJS
NestJS utilise Jest par défaut - un puissant framework de test qui prend en charge tous les types de 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}Concepts de base des tests
Test Doubles - les doublures dans l'application
Parfois, nous devons remplacer de vrais composants par leurs 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 spyOnCannonFire = jest.spyOn(cannon, 'fire');
12
13// Stub - fixed responses
14const tributeServiceStub = {
15 findTribute: () => Promise.resolve(new Tribute('Golden Coin'))
16};Cycle de vie d'un test
1describe('Legionary Legion Management', () => {
2 let legion: LegionaryLegion;
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 LegionaryLegion();
13 legion.addLegionary(new Legionary('Jack Sparrow', '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 crew', () => {
27 expect(legion.size()).toBe(1);
28 expect(legion.getCenturion().name).toBe('Jack Sparrow');
29 });
30});Les tests dans NestJS - des outils spéciaux
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 legionariuszeData = { name: 'Anne Bonny', rank: 'Navigator' };
28 const result = await service.recruitLegionary(legionariuszeData);
29
30 expect(result.name).toBe('Anne Bonny');
31 expect(mockLegionaryRepository.save).toHaveBeenCalledWith(legionariuszeData);
32 });
33});Bonnes pratiques de test
1. Le pattern AAA - 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. Isolation des tests
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. Des noms de tests descriptifs
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});Exemple d'un test de service complet
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});Tester, ce n'est pas seulement vérifier si le code fonctionne - c'est la garantie que ton application survivra à chaque tempête et atteindra sa destination avec tout l'équipage et les tributs ! Dans les modules suivants, nous apprendrons les détails des différents types de tests.
Code de cette leçon : src/testing-intro.ts
1// Testing - Introduction aux tests dans l'Empire romain
2import { Test, TestingModule } from '@nestjs/testing';
3
4// Structure de base des tests dans NestJS
5// Chaque élément du fort doit être vérifié avant de partir en mer !
6
7// 1. DESCRIBE - définit un groupe de tests (comme l'inspection d'une section du fort)
8describe('LegionService', () => {
9
10 // 2. IT - un seul test (comme vérifier un canon)
11 it('should be defined', () => {
12 const service = new LegionService();
13 expect(service).toBeDefined();
14 });
15
16 it('should return all legions', () => {
17 const service = new LegionService();
18 const legions = service.findAll();
19
20 // 3. EXPECT - assertion (le résultat est-il correct ?)
21 expect(legions).toBeInstanceOf(Array);
22 expect(legions.length).toBeGreaterThan(0);
23 });
24
25 it('should create a new legion', () => {
26 const service = new LegionService();
27 const newLegion = service.create({
28 name: 'Legio XIII Gemina',
29 soldiers: 5000,
30 commander: 'Julius Caesar',
31 });
32
33 expect(newLegion).toBeDefined();
34 expect(newLegion.name).toBe('Legio XIII Gemina');
35 expect(newLegion.soldiers).toBe(5000);
36 });
37});
38
39// Exemple de service à tester
40class LegionService {
41 private legions = [
42 { id: 1, name: 'Legio X Equestris', soldiers: 4800, commander: 'Pompey' },
43 { id: 2, name: 'Legio IX Hispana', soldiers: 5200, commander: 'Augustus' },
44 ];
45
46 findAll() {
47 return this.legions;
48 }
49
50 findOne(id: number) {
51 return this.legions.find(l => l.id === id) || null;
52 }
53
54 create(data: { name: string; soldiers: number; commander: string }) {
55 const legion = { id: this.legions.length + 1, ...data };
56 this.legions.push(legion);
57 return legion;
58 }
59}
60
61// Matchers Jest - outils d'inspection
62// expect(value).toBe(expected) - égalité stricte (===)
63// expect(value).toEqual(expected) - égalité profonde
64// expect(value).toBeDefined() - n'est pas undefined
65// expect(value).toBeTruthy() - valeur truthy
66// expect(value).toContain(item) - le tableau contient l'élément
67// expect(fn).toThrow() - la fonction lève une erreur
68Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quels types de tests distingue-t-on dans les applications NestJS ?
2. Les tests unitaires dans NestJS servent à :
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Écris un test vérifiant le bon fonctionnement du service des légions
- Tri vertical
Ordonne le cycle de vie d'un test Jest du début à la fin :
- Clique dans l'ordre
Ordonne la syntaxe correcte d'un bloc describe en Jest :