Testowanie aplikacji NestJS

Testowanie jest kluczowym elementem rozwoju każdej aplikacji, ponieważ pozwala zapewnić niezawodność i stabilność kodu, ułatwia utrzymanie oraz umożliwia wczesne wykrywanie błędów. NestJS, jako framework oparty na Node.js, oferuje wygodne i intuicyjne narzędzia do pisania testów jednostkowych oraz integracyjnych. W tym artykule dowiesz się, jak pisać testy jednostkowe i integracyjne w NestJS, skonfigurować środowisko do testowania oraz korzystać z dostępnych narzędzi.

Testy jednostkowe w NestJS

Testy jednostkowe są kluczowe do zapewnienia, że pojedyncze moduły aplikacji działają zgodnie z oczekiwaniami. W NestJS testowanie jednostkowe jest wspierane przez Jest, popularny framework do testów w ekosystemie JavaScript/TypeScript. Zaczniemy od konfiguracji środowiska do testowania, a następnie stworzymy testy jednostkowe dla kontrolerów i serwisów.

1. Konfiguracja środowiska do testowania

NestJS jest domyślnie zintegrowany z Jest, co znacznie upraszcza proces konfiguracji. Aby rozpocząć pisanie testów, upewnij się, że zainstalowane są odpowiednie pakiety:

				
					npm install --save-dev jest @types/jest ts-jest

				
			

Następnie możesz skonfigurować Jest, korzystając z generatora NestJS:

				
					npx jest --init

				
			

NestJS automatycznie wygeneruje pliki testowe dla nowych serwisów i kontrolerów. Przykładowy plik testowy kontrolera może wyglądać następująco:

				
					import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('getHello', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

				
			

W tym przypadku testujemy endpoint / aplikacji. Używamy supertest, aby symulować żądania HTTP. Test sprawdza, czy odpowiedź zwrócona przez serwer zawiera Hello World! oraz kod odpowiedzi 200.

Omówienie:

  1. Importy:

    • Importujemy Test oraz TestingModule z @nestjs/testing. Są to narzędzia, które pomagają nam w tworzeniu środowiska testowego.
    • Importujemy AppController oraz AppService, które są naszymi obiektami testowanymi.
  2. Opis Testów (describe):

    • describe('AppController', () => {...}): Tworzymy blok opisujący test dla AppController. Jest to grupa testów, które będą związane z tą klasą.
  3. Zmienna appController:

    • Deklarujemy zmienną appController, która przechowuje instancję naszego kontrolera.
  4. Blok beforeEach:

    • beforeEach(async () => {...}): Funkcja ta jest wykonywana przed każdym testem w bloku describe.
    • Tworzymy moduł testowy (TestingModule) za pomocą Test.createTestingModule().
      • Konfigurujemy go, dodając kontroler (AppController) oraz serwis (AppService).
      • Kompilujemy moduł, co umożliwia utworzenie jego instancji.
    • Pobieramy instancję AppController z modułu za pomocą app.get<AppController>(AppController).
  5. Test getHello (it):

    • it('should return "Hello World!"', () => {...}): Funkcja it definiuje pojedynczy test.
    • Sprawdzamy, czy metoda getHello() z AppController zwraca wartość "Hello World!".
    • expect(appController.getHello()).toBe('Hello World!'): Funkcja expect służy do sprawdzenia, czy wynik metody getHello() jest równy oczekiwanej wartości.

2. Mockowanie zależności

Podczas testów integracyjnych często chcemy zastąpić rzeczywiste zależności ich mockami, aby uniknąć wpływu na rzeczywiste dane lub serwisy zewnętrzne. Możemy to zrobić, korzystając z narzędzi @nestjs/testing:

				
					import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

describe('UsersController', () => {
  let usersController: UsersController;
  let usersService: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        {
          provide: UsersService,
          useValue: {
            findAll: jest.fn().mockResolvedValue([{ id: 1, name: 'John Doe' }]),
          },
        },
      ],
    }).compile();

    usersController = module.get<UsersController>(UsersController);
    usersService = module.get<UsersService>(UsersService);
  });

  it('should return an array of users', async () => {
    const result = await usersController.findAll();
    expect(result).toEqual([{ id: 1, name: 'John Doe' }]);
    expect(usersService.findAll).toHaveBeenCalled();
  });
});

				
			

W powyższym przykładzie mockujemy UsersService, aby metoda findAll zawsze zwracała z góry określoną wartość. Dzięki temu test kontrolera UsersController nie jest uzależniony od rzeczywistej implementacji serwisu.

Omówienie:

  1. Importy:

    • Importujemy Test oraz TestingModule z @nestjs/testing do tworzenia modułu testowego.
    • Importujemy UsersService, który będziemy testować.
  2. Opis Testów (describe):

    • describe('UsersService', () => {...}): Tworzymy blok opisujący testy dla UsersService.
  3. Deklaracja service:

    • Deklarujemy zmienną service, która będzie przechowywać instancję UsersService.
  4. Blok beforeEach:

    • Konfigurujemy moduł testowy z dostawcą UsersService.
    • Po skompilowaniu modułu, pobieramy instancję serwisu do testów.
  5. Test should be defined:

    • expect(service).toBeDefined(): Sprawdzamy, czy instancja serwisu została poprawnie utworzona i jest zdefiniowana.
  6. Test should return an array of users:

    • const users = await service.findAll(): Wywołujemy metodę findAll() w serwisie.
    • expect(users).toBeInstanceOf(Array): Sprawdzamy, czy wynik jest tablicą.

3. Testy integracyjne z bazą danych

Jeśli aplikacja korzysta z bazy danych, testy integracyjne mogą wymagać uruchomienia bazy danych testowej. W takim przypadku warto użyć narzędzi takich jak TypeORM w połączeniu z bazą danych in-memory lub użyć kontenerów Docker do uruchamiania bazy danych testowej.

Przykład konfiguracji połączenia z bazą danych do testów:

				
					import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: ':memory:',
      entities: [__dirname + '/' + '../**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

				
			

Używając bazy danych SQLite in-memory, można przeprowadzać testy integracyjne bez konieczności uruchamiania pełnej bazy danych na serwerze.

Podsumowanie

Testowanie aplikacji NestJS jest nieodzownym elementem zapewnienia jakości i stabilności kodu. Pisanie testów jednostkowych pozwala na szybką weryfikację poprawności działania poszczególnych modułów, takich jak kontrolery czy serwisy. Testy integracyjne z kolei pozwalają na sprawdzenie, jak moduły aplikacji współpracują ze sobą, a także testowanie rzeczywistego przepływu danych przez API.

W tym artykule nauczyliśmy się:

  • Jak skonfigurować środowisko do testowania aplikacji NestJS.
  • Jak pisać testy jednostkowe dla kontrolerów i serwisów.
  • Jak przeprowadzać testy integracyjne i jak mockować zależności.
  • Jak korzystać z narzędzi takich jak @nestjs/testing oraz supertest.

Testowanie aplikacji NestJS to inwestycja w jej niezawodność, która szybko się zwraca podczas dalszego rozwoju i utrzymania projektu. Dzięki temu możesz być pewien, że twoja aplikacja będzie działała zgodnie z oczekiwaniami nawet przy dynamicznych zmianach.