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);
});
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:
Importy:
- Importujemy
TestorazTestingModulez@nestjs/testing. Są to narzędzia, które pomagają nam w tworzeniu środowiska testowego. - Importujemy
AppControllerorazAppService, które są naszymi obiektami testowanymi.
- Importujemy
Opis Testów (
describe):describe('AppController', () => {...}): Tworzymy blok opisujący test dlaAppController. Jest to grupa testów, które będą związane z tą klasą.
Zmienna
appController:- Deklarujemy zmienną
appController, która przechowuje instancję naszego kontrolera.
- Deklarujemy zmienną
Blok
beforeEach:beforeEach(async () => {...}): Funkcja ta jest wykonywana przed każdym testem w blokudescribe.- 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.
- Konfigurujemy go, dodając kontroler (
- Pobieramy instancję
AppControllerz modułu za pomocąapp.get<AppController>(AppController).
Test
getHello(it):it('should return "Hello World!"', () => {...}): Funkcjaitdefiniuje pojedynczy test.- Sprawdzamy, czy metoda
getHello()zAppControllerzwraca wartość"Hello World!". expect(appController.getHello()).toBe('Hello World!'): Funkcjaexpectsłuży do sprawdzenia, czy wynik metodygetHello()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);
usersService = module.get(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:
Importy:
- Importujemy
TestorazTestingModulez@nestjs/testingdo tworzenia modułu testowego. - Importujemy
UsersService, który będziemy testować.
- Importujemy
Opis Testów (
describe):describe('UsersService', () => {...}): Tworzymy blok opisujący testy dlaUsersService.
Deklaracja
service:- Deklarujemy zmienną
service, która będzie przechowywać instancjęUsersService.
- Deklarujemy zmienną
Blok
beforeEach:- Konfigurujemy moduł testowy z dostawcą
UsersService. - Po skompilowaniu modułu, pobieramy instancję serwisu do testów.
- Konfigurujemy moduł testowy z dostawcą
Test
should be defined:expect(service).toBeDefined(): Sprawdzamy, czy instancja serwisu została poprawnie utworzona i jest zdefiniowana.
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/testingorazsupertest.
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.