Kurs NestJS · Moduł 6: Obsługa błędów i monitoring

Graceful Shutdown i Recovery - godny odwrót legionów

10 min czytania
W tej lekcji9

Wdrażasz nową wersję. Kubernetes wysyła sygnał zamknięcia, proces znika w pół sekundy, a razem z nim trzy płatności w połowie zapisu. Nawet najpotężniejsze Imperium Rzymskie musiało umieć wycofywać wojska w porządku: gdy Cezar nakazywał odwrót, każdy centurion wiedział, jak zabezpieczyć sprzęt i ewakuować rannych. W NestJS graceful shutdown to ta sama sztuka - zamykanie aplikacji bez chaosu.

Czym jest Graceful Shutdown?

Gdy aplikacja dostaje sygnał zamknięcia (np. SIGTERM podczas deploymentu), musi:

  • dokończyć aktywne żądania - nie porzucaj legionistów w trakcie walki,
  • zamknąć połączenia z bazą danych - zabezpiecz archiwa Imperium,
  • wyczyścić timery i interwały - wycofaj zwiadowców,
  • rozłączyć klientów WebSocket - odwołaj posłańców,
  • zapisać stan aplikacji - sporządź raport z pola bitwy.

Lifecycle Hooks w NestJS

NestJS woła trzy hooki w stałej kolejności. onModuleDestroy() rusza po sygnale, gdy serwer HTTP wciąż obsługuje ruch. beforeApplicationShutdown(signal) dostaje nazwę sygnału; po nim NestJS zamyka serwer HTTP i czeka na trwające żądania. onApplicationShutdown(signal) przychodzi na końcu, gdy serwer już milczy:

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  // Operacje w tle (importy, zadania z kolejki) zgłaszają start i koniec
18  operationStarted() { this.activeOperations++; }
19  operationFinished() { this.activeOperations--; }
20
21  // 1. OnModuleDestroy - pierwszy sygnał do odwrotu
22  // Serwer HTTP nadal przyjmuje żądania
23  async onModuleDestroy() {
24    this.logger.warn('OnModuleDestroy: Rozpoczynam procedurę odwrotu legionów!');
25    // Zatrzymaj to, czego żądania już nie potrzebują (timery, subskrypcje)
26  }
27
28  // 2. BeforeApplicationShutdown - przygotowanie do zamknięcia
29  // Otrzymuje sygnał (SIGTERM, SIGINT) jako argument
30  async beforeApplicationShutdown(signal?: string) {
31    this.logger.warn(
32      `BeforeApplicationShutdown: Sygnał ${signal} - czekam na dokończenie operacji`
33    );
34
35    // Czekaj na operacje w tle, ale najwyżej 8 sekund
36    const deadline = Date.now() + 8000;
37    while (this.activeOperations > 0 && Date.now() < deadline) {
38      this.logger.log(`Pozostało ${this.activeOperations} aktywnych operacji...`);
39      await new Promise(resolve => setTimeout(resolve, 1000));
40    }
41  }
42
43  // 3. OnApplicationShutdown - finalne zamknięcie
44  // Serwer HTTP jest już zamknięty
45  async onApplicationShutdown(signal?: string) {
46    this.logger.warn(`OnApplicationShutdown: Sygnał ${signal} - zamykam bramy Imperium!`);
47    // Ostateczne czyszczenie zasobów
48  }
49}

Na żądania HTTP NestJS czeka sam, więc licznik pilnuje tylko pracy w tle. Limit 8 sekund nie jest przypadkowy: docker stop po 10 sekundach wysyła SIGKILL, którego nie da się obsłużyć.

Aktywacja Shutdown Hooks

Domyślnie NestJS nie nasłuchuje na sygnały systemowe, więc bez jednej linijki w main.ts żaden z tych hooków nie ruszy po 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  // Aktywuj nasłuchiwanie na sygnały SIGTERM i SIGINT
11  app.enableShutdownHooks();
12
13  // SIGTERM - wysyłany przez Docker/Kubernetes przy zatrzymywaniu
14  // SIGINT  - wysyłany przez Ctrl+C w terminalu
15
16  const port = process.env.PORT || 3000;
17  await app.listen(port);
18  logger.log(`Imperium nasłuchuje na porcie ${port}`);
19}
20
21bootstrap();

enableShutdownHooks() wołamy przed listen(). Na Windowsie SIGTERM nie działa wcale, działa za to SIGINT.

Bezpieczne zamykanie połączeń z bazą danych

Bazę zamykamy dopiero w onApplicationShutdown, bo wcześniej serwer HTTP wciąż obsługuje żądania, które z niej korzystają:

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('Zamykam połączenie z bazą danych Imperium...');
16
17    try {
18      // Mongoose - zamknij połączenie gracefully
19      await this.connection.close();
20      this.logger.log('Archiwum Imperium zabezpieczone - połączenie zamknięte.');
21    } catch (error) {
22      this.logger.error('Błąd przy zamykaniu bazy danych:', error.message);
23    }
24  }
25}

Pierwsza wersja tej lekcji zamykała połączenie w onModuleDestroy - za wcześnie, bo żądania obsługiwane w tej chwili traciły bazę. MongooseModule i TypeOrmModule zamykają swoje połączenia same, właśnie w onApplicationShutdown; taki serwis piszesz dla połączeń tworzonych ręcznie.

Czyszczenie timerów i interwałów

Niezamknięte timery mogą blokować zamknięcie procesu Node.js, a ich sprzątanie to dobre zadanie dla 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  // Rejestruj interwały przy tworzeniu
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('Wycofuję zwiadowców - czyszczę timery i interwały...');
25
26    this.intervals.forEach(interval => clearInterval(interval));
27    this.logger.log(`Wyczyszczono ${this.intervals.length} interwałów`);
28
29    this.timeouts.forEach(timeout => clearTimeout(timeout));
30    this.logger.log(`Wyczyszczono ${this.timeouts.length} timeoutów`);
31
32    this.intervals = [];
33    this.timeouts = [];
34  }
35}

Serwis zapamiętuje każdy timer, który utworzył, i przy odwrocie czyści wszystkie naraz.

Health Check Recovery Pattern

Gdy aplikacja przeżywa problemy, potrzebny jest mechanizm recovery - automatycznego powrotu do zdrowia:

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      `Awaria komponentu ${component}! Licznik: ${this.failureCount}/${this.MAX_FAILURES}`
15    );
16
17    if (this.failureCount >= this.MAX_FAILURES) {
18      this.isHealthy = false;
19      this.logger.error('Imperium w stanie krytycznym! Uruchamiam procedurę recovery...');
20      this.startRecovery();
21    }
22  }
23
24  private async startRecovery() {
25    this.logger.warn('Procedura recovery: próba przywrócenia usług...');
26
27    try {
28      await this.reconnectDatabase();
29      await this.clearCache();
30      this.failureCount = 0;
31      this.isHealthy = true;
32      this.logger.log('Recovery zakończony sukcesem!');
33    } catch (error) {
34      this.logger.error('Recovery nie powiódł się:', error.message);
35    }
36  }
37
38  private async reconnectDatabase() {
39    this.logger.log('Ponowne łączenie z archiwum...');
40    await new Promise(resolve => setTimeout(resolve, 1000));
41  }
42
43  private async clearCache() {
44    this.logger.log('Czyszczenie skrytek 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}

Po trzech awariach serwis oznacza się jako chory i próbuje się naprawić. startRecovery() działa w tle, bez await, więc zgłaszający awarię nie czeka.

Circuit Breaker Pattern - zabezpieczenie murów Imperium

Circuit Breaker chroni aplikację przed kaskadowymi awariami - jak zamykanie bram miasta, gdy wróg atakuje:

1// circuit-breaker.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4enum CircuitState {
5  CLOSED = 'CLOSED',       // Wszystko działa - bramy otwarte
6  OPEN = 'OPEN',           // Awaria - bramy zamknięte
7  HALF_OPEN = 'HALF_OPEN'  // Testujemy - bramy uchylone
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 sekund
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 - wysyłamy posłańca na próbę...');
24      } else {
25        throw new Error('Circuit OPEN - bramy Imperium zamknięte!');
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 - bramy ponownie otwarte!');
35      }
36      this.failureCount = 0; // liczymy tylko awarie z rzędu
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 po ${this.failureCount} awariach - zamykam bramy!`
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}

Po pięciu awariach z rzędu bramy się zamykają i żądania od razu dostają błąd, zamiast czekać na martwy serwis. Po 30 sekundach próbne wywołanie decyduje, czy wracamy do CLOSED. Wcześniej licznik nie zerował się po sukcesie, więc pięć awarii rozrzuconych po tygodniu też otwierało obwód.

Obsługa sygnałów SIGTERM i SIGINT

Warto wiedzieć, skąd przychodzą sygnały:

  • SIGTERM - wysyła go docker stop (SIGKILL po 10 s) i Kubernetes (domyślnie po 30 s),
  • SIGINT - Ctrl+C w terminalu; z enableShutdownHooks() też uruchamia pełny odwrót.
1// main.ts - własne reakcje na sygnały, obok enableShutdownHooks()
2process.once('SIGTERM', () => {
3  console.log('Otrzymano SIGTERM - rozpoczynam graceful shutdown...');
4});
5
6process.once('SIGINT', () => {
7  console.log('Otrzymano SIGINT - rozpoczynam graceful shutdown...');
8});
9
10// Złapanie nieobsłużonych wyjątków
11process.on('uncaughtException', (error) => {
12  console.error('Nieobsłużony wyjątek:', error);
13  process.exit(1);
14});
15
16process.on('unhandledRejection', (reason) => {
17  console.error('Nieobsłużona obietnica:', reason);
18  process.kill(process.pid, 'SIGTERM'); // uruchom odwrót zamiast udawać, że nic się nie stało
19});

once ma znaczenie: stały listener SIGTERM wyłącza domyślne wyjście Node.js, a NestJS po sprzątaniu ponawia sygnał, żeby zakończyć proces - z listenerem on aplikacja potrafi zawisnąć. Samo nasłuchiwanie unhandledRejection też wyłącza domyślny crash, więc świadomie uruchamiamy odwrót.

Sprawdź odwrót, zanim nadejdzie

Recovery, którego nie przetestowałeś, jest tylko nadzieją. W testach Jest describe grupuje przypadki, it opisuje jeden, a expect(result).toEqual(expected) porównuje wynik z oczekiwanym. beforeEach uruchamia się przed każdym testem, beforeAll raz przed wszystkimi w bloku. Od NestJS 12 nowe projekty dostają Vitest z tym samym API, tylko zamiast jest.fn() piszesz vi.fn().

Testować będziemy serwis, który przy braku legionu rzuca HttpException z obiektem odpowiedzi i kodem statusu:

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}

W teście jednostkowym prawdziwe repozytorium zastępujemy atrapą: getRepositoryToken(Legion) to token, pod którym @InjectRepository szuka repozytorium, a jest.fn() tworzy funkcję, której wynik ustalamy sami:

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('zwraca legion o podanym id', 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('rzuca HttpException, gdy legionu nie ma', async () => {
32    repo.findOne.mockResolvedValue(null);
33
34    await expect(service.findOne(99)).rejects.toThrow(HttpException);
35  });
36});

Komentarze pokazują wzorzec AAA: Arrange przygotowuje dane i atrapy, Act wykonuje operację, Assert sprawdza wynik. beforeEach buduje świeży moduł przed każdym testem, więc atrapy nie przenoszą stanu między przypadkami.

Test end-to-end uruchamia całą aplikację i wysyła prawdziwe żądanie przez 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 odpowiada 200', () => {
20    return request(app.getHttpServer()).get('/health').expect(200);
21  });
22
23  afterAll(async () => {
24    await app.close();
25  });
26});

Aplikację budujemy raz, w beforeAll, bo to kosztowne. app.close() w afterAll uruchamia te same hooki zamykania, więc test sprawdza też odwrót.

Filtr wyjątków z pierwszej lekcji modułu testujemy bez serwera, podając atrapę ArgumentsHost:

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() pozwala na łańcuch response.status(404).json(...), a toHaveBeenCalledWith(404) sprawdza, z jakim argumentem wywołano atrapę.

Jest jeszcze haczyk: filtr zarejestrowany przez app.useGlobalFilters() w main.ts nie działa w teście e2e, bo test nie uruchamia main.ts. Rejestracja w module z tokenem APP_FILTER działa wszędzie i pozwala wstrzykiwać zależności:

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

Raport pokrycia kodu dostaniesz przez npm run test -- --coverage albo gotowy skrypt test:cov.

Polecam Ci prosty nawyk: każdy hook zamykania i każdy filtr niech ma choć jeden test, bo awarie zdarzają się rzadko i nigdy wtedy, gdy masz czas na ręczne sprawdzanie. W projekcie kończącym moduł połączysz filtry, logi, health checki i testy w jeden system monitorowania.

Pamiętaj: szczęście sprzyja przygotowanym - aplikacja, która umie się zamknąć i podnieść po awarii, jest jak legion gotowy na każdą ewentualność.

Kod do tej lekcji: 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. Serwis 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  // Sledz aktywne zadania
17  incrementRequests() { this.activeRequests++; }
18  decrementRequests() { this.activeRequests--; }
19
20  // Rejestruj interwaly do czyszczenia
21  registerInterval(interval: any) {
22    this.intervals.push(interval);
23  }
24
25  // OnModuleDestroy - pierwszy krok odwrotu
26  async onModuleDestroy() {
27    this.logger.warn('OnModuleDestroy: Rozpoczynam procedure odwrotu!');
28
29    // Wyczysc interwaly
30    this.intervals.forEach(i => clearInterval(i));
31    this.logger.log('Wyczyszczono ' + this.intervals.length + ' interwalow');
32    this.intervals = [];
33  }
34
35  // BeforeApplicationShutdown - czekaj na aktywne operacje
36  async beforeApplicationShutdown(signal?: string) {
37    this.logger.warn('BeforeApplicationShutdown: Sygnal ' + signal);
38
39    // Czekaj na zakonczenie aktywnych zadan
40    let waitCount = 0;
41    while (this.activeRequests > 0 && waitCount < 10) {
42      this.logger.log('Aktywne operacje: ' + this.activeRequests);
43      await new Promise(r => setTimeout(r, 1000));
44      waitCount++;
45    }
46
47    this.logger.log('Wszystkie operacje zakonczone - bramy zamkniete');
48  }
49}
50
51// ===========================================
52// 2. Circuit Breaker Pattern
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. Demonstracja
101// ===========================================
102
103const service = new LegionShutdownService();
104
105// Symulacja aktywnych zadan
106service.incrementRequests();
107service.incrementRequests();
108console.log('=== Graceful Shutdown ===');
109console.log('Aktywne zadania przed shutdown: 2');
110
111service.decrementRequests();
112service.decrementRequests();
113console.log('Aktywne zadania po zakonczeniu: 0');
114console.log('');
115
116// Circuit Breaker
117const breaker = new CircuitBreaker();
118console.log('=== Circuit Breaker ===');
119console.log('Stan:', breaker.getState().state);
120console.log('');
121console.log('Hooki: OnModuleDestroy -> BeforeApplicationShutdown -> OnApplicationShutdown');
122console.log('Sygnaly: SIGTERM (Docker), SIGINT (Ctrl+C)');
123console.log('app.enableShutdownHooks() - aktywacja w main.ts');
124

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co robi hook OnModuleDestroy w NestJS?

  2. 2. Jak aktywować nasłuchiwanie na sygnały systemowe (SIGTERM/SIGINT) w NestJS?

To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij serwis implementujący OnModuleDestroy i OnApplicationShutdown: w onModuleDestroy() wyczyść interwały (clearInterval), a w onApplicationShutdown(), gdy serwer HTTP nie przyjmuje już żądań, zamknij połączenie z bazą danych (this.connection.close())

  • Układanie w pionie

    Uporządkuj hooki lifecycle NestJS w kolejności wywoływania przy zamykaniu aplikacji

  • Klikanie w kolejności

    Ułóż kod aktywacji shutdown hooks w main.ts

  • Edytor kodu

    Uzupełnij E2E test, który tworzy NestApplication z Test.createTestingModule, inicjalizuje ją (app.init()) i używa request(app.getHttpServer()).get('/health').expect(200)

  • Układanie w poziomie

    Ułóż składnię asercji Jest od oczekiwanej wartości do matchera

  • Edytor kodu

    Uzupełnij test it('should catch NotFoundException'), który tworzy mock response i request, wywołuje filter.catch(new NotFoundException(), mockHost) i sprawdza czy response.status został wywołany z 404

  • Układanie w pionie

    Uporządkuj etapy wzorca AAA (Arrange-Act-Assert) stosowanego w testach

  • Klikanie w kolejności

    Ułóż kroki rejestracji globalnego Exception Filter w main.ts

  • Układanie w poziomie

    Ułóż składnię rejestracji globalnego filtra z Dependency Injection w module

  • Układanie w pionie

    Uporządkuj kroki konfiguracji modułu z Exception Filters od tworzenia do uruchomienia

  • Układanie w poziomie

    Ułóż składnię rzucania HttpException z obiektem odpowiedzi i kodem statusu

  • Układanie w poziomie

    Ułóż składnię tworzenia mock repository z jest.fn() od provide do useValue

Przydatne artykuły