Cours NestJS · Module 6: Gestion des erreurs et monitoring

Graceful Shutdown et récupération - Une retraite ordonnée des légions

12 min de lecture
Dans cette leçon9

Tu déploies une nouvelle version. Kubernetes envoie un signal d'arrêt, le processus disparaît en une demi-seconde, et avec lui trois paiements à moitié enregistrés. Même le puissant Empire romain devait savoir retirer ses troupes en bon ordre : quand César ordonnait la retraite, chaque centurion savait comment sécuriser le matériel et évacuer les blessés. Dans NestJS, le graceful shutdown est le même art : arrêter l'application sans laisser de chaos derrière soi.

Qu'est-ce que le Graceful Shutdown ?

Quand l'application reçoit un signal d'arrêt (par ex. SIGTERM pendant un déploiement), elle doit :

  • terminer les requêtes actives - n'abandonne pas les légionnaires en pleine bataille,
  • fermer les connexions à la base de données - sécurise les archives de l'Empire,
  • nettoyer les timers et les intervalles - rappelle les éclaireurs,
  • déconnecter les clients WebSocket - rappelle les messagers,
  • sauvegarder l'état de l'application - rédige un rapport du champ de bataille.

Les lifecycle hooks dans NestJS

NestJS appelle trois hooks dans un ordre fixe. onModuleDestroy() s'exécute après le signal, pendant que le serveur HTTP traite encore le trafic. beforeApplicationShutdown(signal) reçoit le nom du signal ; après lui, NestJS ferme le serveur HTTP et attend les requêtes en cours. onApplicationShutdown(signal) arrive en dernier, quand le serveur s'est tu :

1// graceful-shutdown.service.ts
2import {
3  Injectable,
4  OnModuleDestroy,
5  BeforeApplicationShutdown,
6  OnApplicationShutdown,
7  Logger
8} from '@nestjs/common';
9
10@Injectable()
11export class LegionShutdownService
12  implements OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown
13{
14  private readonly logger = new Logger(LegionShutdownService.name);
15  private activeOperations = 0;
16
17  // Les opérations en arrière-plan (imports, tâches de file) signalent leur début et leur fin
18  operationStarted() { this.activeOperations++; }
19  operationFinished() { this.activeOperations--; }
20
21  // 1. OnModuleDestroy - le premier signal de la retraite
22  // Le serveur HTTP accepte encore des requêtes
23  async onModuleDestroy() {
24    this.logger.warn('OnModuleDestroy : je lance la procédure de retraite des légions !');
25    // Arrête ce dont les requêtes n'ont plus besoin (timers, abonnements)
26  }
27
28  // 2. BeforeApplicationShutdown - la préparation à l'arrêt
29  // Reçoit le signal (SIGTERM, SIGINT) en argument
30  async beforeApplicationShutdown(signal?: string) {
31    this.logger.warn(
32      `BeforeApplicationShutdown : signal ${signal} - j'attends la fin des opérations`
33    );
34
35    // Attends les opérations en arrière-plan, mais 8 secondes au maximum
36    const deadline = Date.now() + 8000;
37    while (this.activeOperations > 0 && Date.now() < deadline) {
38      this.logger.log(`Encore ${this.activeOperations} opérations actives...`);
39      await new Promise(resolve => setTimeout(resolve, 1000));
40    }
41  }
42
43  // 3. OnApplicationShutdown - l'arrêt final
44  // Le serveur HTTP est déjà fermé
45  async onApplicationShutdown(signal?: string) {
46    this.logger.warn(`OnApplicationShutdown : signal ${signal} - je ferme les portes de l'Empire !`);
47    // Nettoyage final des ressources
48  }
49}

NestJS attend lui-même les requêtes HTTP, le compteur ne surveille donc que le travail en arrière-plan. La limite de 8 secondes n'est pas un hasard : docker stop envoie SIGKILL après 10 secondes, et celui-là ne se gère pas.

Activer les shutdown hooks

Par défaut, NestJS n'écoute pas les signaux système : sans une ligne dans main.ts, aucun de ces hooks ne se lance après SIGTERM :

1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4import { Logger } from '@nestjs/common';
5
6async function bootstrap() {
7  const app = await NestFactory.create(AppModule);
8  const logger = new Logger('Bootstrap');
9
10  // Active l'écoute des signaux SIGTERM et SIGINT
11  app.enableShutdownHooks();
12
13  // SIGTERM - envoyé par Docker/Kubernetes à l'arrêt
14  // SIGINT  - envoyé par Ctrl+C dans le terminal
15
16  const port = process.env.PORT || 3000;
17  await app.listen(port);
18  logger.log(`L'Empire écoute sur le port ${port}`);
19}
20
21bootstrap();

On appelle enableShutdownHooks() avant listen(). Sous Windows, SIGTERM ne fonctionne pas du tout, mais SIGINT oui.

Fermer les connexions à la base de données en toute sécurité

On ne ferme la base qu'à onApplicationShutdown, car avant cela le serveur HTTP traite encore des requêtes qui l'utilisent :

1// database-shutdown.service.ts
2import { Injectable, OnApplicationShutdown, Logger } from '@nestjs/common';
3import { InjectConnection } from '@nestjs/mongoose';
4import { Connection } from 'mongoose';
5
6@Injectable()
7export class DatabaseShutdownService implements OnApplicationShutdown {
8  private readonly logger = new Logger(DatabaseShutdownService.name);
9
10  constructor(
11    @InjectConnection() private readonly connection: Connection,
12  ) {}
13
14  async onApplicationShutdown() {
15    this.logger.warn("Je ferme la connexion à la base de données de l'Empire...");
16
17    try {
18      // Mongoose - ferme la connexion proprement
19      await this.connection.close();
20      this.logger.log("Archives de l'Empire sécurisées - connexion fermée.");
21    } catch (error) {
22      this.logger.error('Erreur à la fermeture de la base de données :', error.message);
23    }
24  }
25}

La première version de cette leçon fermait la connexion dans onModuleDestroy : trop tôt, car les requêtes traitées à ce moment-là perdaient leur base. MongooseModule et TypeOrmModule ferment eux-mêmes leurs connexions, justement dans onApplicationShutdown ; tu écris un service comme celui-ci pour les connexions que tu crées à la main.

Nettoyer les timers et les intervalles

Des timers laissés actifs peuvent empêcher le processus Node.js de se terminer, et leur nettoyage est un bon travail pour onModuleDestroy :

1// scheduler-cleanup.service.ts
2import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
3
4@Injectable()
5export class SchedulerCleanupService implements OnModuleDestroy {
6  private readonly logger = new Logger(SchedulerCleanupService.name);
7  private intervals: NodeJS.Timeout[] = [];
8  private timeouts: NodeJS.Timeout[] = [];
9
10  // Enregistre les intervalles à leur création
11  registerInterval(callback: () => void, ms: number): NodeJS.Timeout {
12    const interval = setInterval(callback, ms);
13    this.intervals.push(interval);
14    return interval;
15  }
16
17  registerTimeout(callback: () => void, ms: number): NodeJS.Timeout {
18    const timeout = setTimeout(callback, ms);
19    this.timeouts.push(timeout);
20    return timeout;
21  }
22
23  async onModuleDestroy() {
24    this.logger.warn('Je rappelle les éclaireurs - je nettoie les timers et les intervalles...');
25
26    this.intervals.forEach(interval => clearInterval(interval));
27    this.logger.log(`${this.intervals.length} intervalles nettoyés`);
28
29    this.timeouts.forEach(timeout => clearTimeout(timeout));
30    this.logger.log(`${this.timeouts.length} timeouts nettoyés`);
31
32    this.intervals = [];
33    this.timeouts = [];
34  }
35}

Le service mémorise chaque timer qu'il a créé et les nettoie tous d'un coup pendant la retraite.

Health Check Recovery Pattern

Quand l'application traverse des difficultés, il lui faut un mécanisme de recovery, c'est-à-dire un retour automatique à la santé :

1// health-recovery.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4@Injectable()
5export class HealthRecoveryService {
6  private readonly logger = new Logger(HealthRecoveryService.name);
7  private isHealthy = true;
8  private failureCount = 0;
9  private readonly MAX_FAILURES = 3;
10
11  reportFailure(component: string) {
12    this.failureCount++;
13    this.logger.warn(
14      `Panne du composant ${component} ! Compteur : ${this.failureCount}/${this.MAX_FAILURES}`
15    );
16
17    if (this.failureCount >= this.MAX_FAILURES) {
18      this.isHealthy = false;
19      this.logger.error("L'Empire est dans un état critique ! Je lance la procédure de recovery...");
20      this.startRecovery();
21    }
22  }
23
24  private async startRecovery() {
25    this.logger.warn('Procédure de recovery : tentative de rétablir les services...');
26
27    try {
28      await this.reconnectDatabase();
29      await this.clearCache();
30      this.failureCount = 0;
31      this.isHealthy = true;
32      this.logger.log('Recovery terminé avec succès !');
33    } catch (error) {
34      this.logger.error('Le recovery a échoué :', error.message);
35    }
36  }
37
38  private async reconnectDatabase() {
39    this.logger.log('Reconnexion aux archives...');
40    await new Promise(resolve => setTimeout(resolve, 1000));
41  }
42
43  private async clearCache() {
44    this.logger.log('Nettoyage des réserves de cache...');
45    await new Promise(resolve => setTimeout(resolve, 500));
46  }
47
48  getHealthStatus() {
49    return {
50      healthy: this.isHealthy,
51      failureCount: this.failureCount,
52      maxFailures: this.MAX_FAILURES,
53    };
54  }
55}

Après trois pannes, le service se déclare malade et tente de se réparer. startRecovery() tourne en arrière-plan, sans await, donc celui qui signale la panne n'attend pas.

Circuit Breaker Pattern - protéger les murs de l'Empire

Un Circuit Breaker protège l'application contre les défaillances en cascade, comme fermer les portes de la ville quand l'ennemi attaque :

1// circuit-breaker.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4enum CircuitState {
5  CLOSED = 'CLOSED',       // Tout fonctionne - portes ouvertes
6  OPEN = 'OPEN',           // Panne - portes fermées
7  HALF_OPEN = 'HALF_OPEN'  // Test - portes entrouvertes
8}
9
10@Injectable()
11export class CircuitBreakerService {
12  private readonly logger = new Logger(CircuitBreakerService.name);
13  private state = CircuitState.CLOSED;
14  private failureCount = 0;
15  private lastFailureTime = 0;
16  private readonly FAILURE_THRESHOLD = 5;
17  private readonly RECOVERY_TIMEOUT = 30000; // 30 secondes
18
19  async execute<T>(operation: () => Promise<T>): Promise<T> {
20    if (this.state === CircuitState.OPEN) {
21      if (Date.now() - this.lastFailureTime > this.RECOVERY_TIMEOUT) {
22        this.state = CircuitState.HALF_OPEN;
23        this.logger.warn('Circuit HALF_OPEN - on envoie un messager à titre d’essai...');
24      } else {
25        throw new Error("Circuit OPEN - les portes de l'Empire sont fermées !");
26      }
27    }
28
29    try {
30      const result = await operation();
31
32      if (this.state === CircuitState.HALF_OPEN) {
33        this.state = CircuitState.CLOSED;
34        this.logger.log('Circuit CLOSED - les portes sont de nouveau ouvertes !');
35      }
36      this.failureCount = 0; // on ne compte que les pannes consécutives
37
38      return result;
39    } catch (error) {
40      this.failureCount++;
41      this.lastFailureTime = Date.now();
42
43      if (this.failureCount >= this.FAILURE_THRESHOLD) {
44        this.state = CircuitState.OPEN;
45        this.logger.error(
46          `Circuit OPEN après ${this.failureCount} pannes - je ferme les portes !`
47        );
48      }
49
50      throw error;
51    }
52  }
53
54  getState() {
55    return {
56      state: this.state,
57      failureCount: this.failureCount,
58      threshold: this.FAILURE_THRESHOLD,
59    };
60  }
61}

Après cinq pannes consécutives, les portes se ferment et les requêtes échouent tout de suite au lieu d'attendre un service mort. Après 30 secondes, un appel d'essai décide si l'on revient à CLOSED. Avant, le compteur ne se remettait pas à zéro après un succès, donc cinq pannes réparties sur une semaine ouvraient aussi le circuit.

Gérer SIGTERM et SIGINT

Il vaut la peine de savoir d'où viennent les signaux :

  • SIGTERM - envoyé par docker stop (SIGKILL après 10 s) et par Kubernetes (après 30 s par défaut),
  • SIGINT - Ctrl+C dans le terminal ; avec enableShutdownHooks(), il lance aussi la retraite complète.
1// main.ts - tes propres réactions aux signaux, à côté de enableShutdownHooks()
2process.once('SIGTERM', () => {
3  console.log('SIGTERM reçu - je lance le graceful shutdown...');
4});
5
6process.once('SIGINT', () => {
7  console.log('SIGINT reçu - je lance le graceful shutdown...');
8});
9
10// Capturer les exceptions non gérées
11process.on('uncaughtException', (error) => {
12  console.error('Exception non gérée :', error);
13  process.exit(1);
14});
15
16process.on('unhandledRejection', (reason) => {
17  console.error('Promesse rejetée non gérée :', reason);
18  process.kill(process.pid, 'SIGTERM'); // lance la retraite au lieu de faire comme si de rien n'était
19});

once compte : un écouteur SIGTERM permanent supprime la sortie par défaut de Node.js, et après le nettoyage NestJS renvoie le signal pour terminer le processus ; avec un écouteur on, l'application peut rester bloquée. Le simple fait d'écouter unhandledRejection désactive aussi le crash par défaut, donc on lance la retraite volontairement.

Teste la retraite avant qu'elle n'arrive

Un recovery que tu n'as pas testé n'est qu'un espoir. Dans les tests Jest, describe regroupe les cas, it en décrit un, et expect(result).toEqual(expected) compare le résultat à la valeur attendue. beforeEach s'exécute avant chaque test, beforeAll une seule fois avant tous les tests du bloc. Depuis NestJS 12, les nouveaux projets reçoivent Vitest avec la même API, sauf qu'au lieu de jest.fn() tu écris vi.fn().

Nous allons tester un service qui lève une HttpException avec un objet de réponse et un code de statut quand une légion manque :

1// legions.service.ts
2@Injectable()
3export class LegionsService {
4  constructor(
5    @InjectRepository(Legion) private readonly repo: Repository<Legion>,
6  ) {}
7
8  async findOne(id: number) {
9    const legion = await this.repo.findOne({ where: { id } });
10    if (!legion) {
11      throw new HttpException(
12        { message: 'Legion not found', error: 'Not Found' },
13        HttpStatus.NOT_FOUND);
14    }
15    return legion;
16  }
17}

Dans un test unitaire, on remplace le vrai repository par une doublure : getRepositoryToken(Legion) est le jeton sous lequel @InjectRepository cherche le repository, et jest.fn() crée une fonction dont on fixe nous-mêmes le résultat :

1// legions.service.spec.ts
2describe('LegionsService', () => {
3  let service: LegionsService;
4  let repo: { findOne: jest.Mock; save: jest.Mock };
5
6  beforeEach(async () => {
7    repo = { findOne: jest.fn(), save: jest.fn() };
8
9    const moduleRef = await Test.createTestingModule({
10      providers: [
11        LegionsService,
12        { provide: getRepositoryToken(Legion), useValue: repo },
13      ],
14    }).compile();
15
16    service = moduleRef.get(LegionsService);
17  });
18
19  it("renvoie la légion avec l'id donné", async () => {
20    // Arrange
21    const legion = { id: 9, name: 'Legio IX Hispana' };
22    repo.findOne.mockResolvedValue(legion);
23
24    // Act
25    const result = await service.findOne(9);
26
27    // Assert
28    expect(result).toEqual(legion);
29  });
30
31  it("lève HttpException quand la légion n'existe pas", async () => {
32    repo.findOne.mockResolvedValue(null);
33
34    await expect(service.findOne(99)).rejects.toThrow(HttpException);
35  });
36});

Les commentaires montrent le pattern AAA : Arrange prépare les données et les mocks, Act exécute l'opération, Assert vérifie le résultat. beforeEach construit un module neuf avant chaque test, donc les mocks ne transportent pas d'état d'un cas à l'autre.

Un test end-to-end lance toute l'application et envoie une vraie requête avec supertest :

1// test/health.e2e-spec.ts
2import request from 'supertest';
3import { Test } from '@nestjs/testing';
4import { INestApplication } from '@nestjs/common';
5import { AppModule } from '../src/app.module';
6
7describe('Health (e2e)', () => {
8  let app: INestApplication;
9
10  beforeAll(async () => {
11    const moduleFixture = await Test.createTestingModule({
12      imports: [AppModule],
13    }).compile();
14
15    app = moduleFixture.createNestApplication();
16    await app.init();
17  });
18
19  it('GET /health répond 200', () => {
20    return request(app.getHttpServer()).get('/health').expect(200);
21  });
22
23  afterAll(async () => {
24    await app.close();
25  });
26});

On construit l'application une seule fois, dans beforeAll, parce que c'est coûteux. app.close() dans afterAll exécute les mêmes hooks d'arrêt, donc le test vérifie aussi la retraite.

On teste le filtre d'exceptions de la première leçon du module sans serveur, en lui passant un ArgumentsHost simulé :

1// legion-http-exception.filter.spec.ts
2it('should catch NotFoundException', () => {
3  const filter = new LegionHttpExceptionFilter();
4  const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
5  const mockReq = { url: '/legions/99' };
6  const mockHost = {
7    switchToHttp: () => ({
8      getResponse: () => mockRes,
9      getRequest: () => mockReq,
10    }),
11  } as unknown as ArgumentsHost;
12
13  filter.catch(new NotFoundException(), mockHost);
14
15  expect(mockRes.status).toHaveBeenCalledWith(404);
16});

mockReturnThis() permet la chaîne response.status(404).json(...), et toHaveBeenCalledWith(404) vérifie avec quel argument le mock a été appelé.

Il reste un piège : un filtre enregistré avec app.useGlobalFilters() dans main.ts ne fonctionne pas dans un test e2e, car le test n'exécute pas main.ts. L'enregistrement dans un module avec le jeton APP_FILTER fonctionne partout et permet l'injection de dépendances :

1// app.module.ts - un filtre global avec Dependency Injection
2@Module({
3  providers: [
4    { provide: APP_FILTER, useClass: LegionHttpExceptionFilter },
5  ],
6})
7export class AppModule {}

Tu obtiens un rapport de couverture du code avec npm run test -- --coverage ou avec le script tout prêt test:cov.

Je te recommande une habitude simple : donne au moins un test à chaque hook d'arrêt et à chaque filtre, car les pannes sont rares et n'arrivent jamais quand tu as le temps de vérifier à la main. Dans le projet qui clôt le module, tu réuniras filtres, logs, health checks et tests en un seul système de surveillance.

Retiens : la chance sourit aux préparés - une application qui sait s'arrêter et se relever après une panne est comme une légion prête à toute éventualité.

Code de cette leçon : src/graceful-shutdown.service.ts
1// Graceful Shutdown i Recovery - Godny Odwrot Legionow
2import { Injectable, OnModuleDestroy, BeforeApplicationShutdown, Logger } from '@nestjs/common';
3
4// ===========================================
5// 1. Service de Graceful Shutdown
6// ===========================================
7
8@Injectable()
9export class LegionShutdownService
10  implements OnModuleDestroy, BeforeApplicationShutdown
11{
12  private readonly logger = new Logger(LegionShutdownService.name);
13  private activeRequests = 0;
14  private intervals: any[] = [];
15
16  // Suivre les requêtes actives
17  incrementRequests() { this.activeRequests++; }
18  decrementRequests() { this.activeRequests--; }
19
20  // Enregistrer les intervalles à nettoyer
21  registerInterval(interval: any) {
22    this.intervals.push(interval);
23  }
24
25  // OnModuleDestroy - première étape de la retraite
26  async onModuleDestroy() {
27    this.logger.warn('OnModuleDestroy: Démarrage de la procédure de retraite !');
28
29    // Nettoyer les intervalles
30    this.intervals.forEach(i => clearInterval(i));
31    this.logger.log('Wyczyszczono ' + this.intervals.length + ' interwalow');
32    this.intervals = [];
33  }
34
35  // BeforeApplicationShutdown - attendre les opérations actives
36  async beforeApplicationShutdown(signal?: string) {
37    this.logger.warn('BeforeApplicationShutdown: Sygnal ' + signal);
38
39    // Attendre la fin des requêtes actives
40    let waitCount = 0;
41    while (this.activeRequests > 0 && waitCount < 10) {
42      this.logger.log('Active operacje: ' + this.activeRequests);
43      await new Promise(r => setTimeout(r, 1000));
44      waitCount++;
45    }
46
47    this.logger.log('Toutes les opérations terminées - portes fermées');
48  }
49}
50
51// ===========================================
52// 2. Le pattern Circuit Breaker
53// ===========================================
54
55enum CircuitState {
56  CLOSED = 'CLOSED',
57  OPEN = 'OPEN',
58  HALF_OPEN = 'HALF_OPEN'
59}
60
61class CircuitBreaker {
62  private state = CircuitState.CLOSED;
63  private failures = 0;
64  private lastFailure = 0;
65  private readonly threshold = 5;
66  private readonly timeout = 30000;
67
68  async execute<T>(operation: () => Promise<T>): Promise<T> {
69    if (this.state === CircuitState.OPEN) {
70      if (Date.now() - this.lastFailure > this.timeout) {
71        this.state = CircuitState.HALF_OPEN;
72      } else {
73        throw new Error('Circuit OPEN - bramy zamkniete!');
74      }
75    }
76
77    try {
78      const result = await operation();
79      if (this.state === CircuitState.HALF_OPEN) {
80        this.state = CircuitState.CLOSED;
81        this.failures = 0;
82      }
83      return result;
84    } catch (error) {
85      this.failures++;
86      this.lastFailure = Date.now();
87      if (this.failures >= this.threshold) {
88        this.state = CircuitState.OPEN;
89      }
90      throw error;
91    }
92  }
93
94  getState() {
95    return { state: this.state, failures: this.failures };
96  }
97}
98
99// ===========================================
100// 3. Démonstration
101// ===========================================
102
103const service = new LegionShutdownService();
104
105// Simulation de requêtes actives
106service.incrementRequests();
107service.incrementRequests();
108console.log('=== Graceful Shutdown ===');
109console.log("Tâches actives avant l'arrêt : 2");
110
111service.decrementRequests();
112service.decrementRequests();
113console.log('Tâches actives après achèvement : 0');
114console.log('');
115
116// Circuit Breaker
117const breaker = new CircuitBreaker();
118console.log('=== Circuit Breaker ===');
119console.log('État :', breaker.getState().state);
120console.log('');
121console.log('Hooks : OnModuleDestroy -> BeforeApplicationShutdown -> OnApplicationShutdown');
122console.log('Signaux : SIGTERM (Docker), SIGINT (Ctrl+C)');
123console.log('app.enableShutdownHooks() - activation dans main.ts');
124

Tu 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. 1. Que fait le hook OnModuleDestroy dans NestJS ?

  2. 2. Comment active-t-on l'écoute des signaux système (SIGTERM/SIGINT) dans NestJS ?

Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète le service implémentant OnModuleDestroy et OnApplicationShutdown : dans onModuleDestroy(), efface les intervalles (clearInterval), et dans onApplicationShutdown(), quand le serveur HTTP n'accepte plus de requêtes, ferme la connexion à la base de données (this.connection.close())

  • Tri vertical

    Ordonne les hooks de cycle de vie de NestJS dans leur ordre d'exécution lors de l'arrêt de l'application

  • Clique dans l'ordre

    Ordonne le code d'activation des shutdown hooks dans main.ts

  • Éditeur de code

    Complète le test E2E qui crée une NestApplication avec Test.createTestingModule, l'initialise (app.init()) et utilise request(app.getHttpServer()).get('/health').expect(200)

  • Tri horizontal

    Ordonne la syntaxe d'assertion Jest de la valeur attendue au matcher

  • Éditeur de code

    Complète le test it('should catch NotFoundException') qui crée un mock de response et request, appelle filter.catch(new NotFoundException(), mockHost) et vérifie que response.status a été appelé avec 404

  • Tri vertical

    Ordonne les étapes du pattern AAA (Arrange-Act-Assert) utilisé dans les tests

  • Clique dans l'ordre

    Ordonne les étapes d'enregistrement d'un Exception Filter global dans main.ts

  • Tri horizontal

    Ordonne la syntaxe d'enregistrement d'un filtre global avec l'injection de dépendances dans un module

  • Tri vertical

    Ordonne les étapes de configuration du module avec les Exception Filters, de la création au démarrage

  • Tri horizontal

    Ordonne la syntaxe pour lever une HttpException avec un objet de réponse et un code de statut

  • Tri horizontal

    Ordonne la syntaxe de création d'un mock repository avec jest.fn(), de provide à useValue