Testing bildet das Fundament für robuste und wartbare Software. Wenn wir an Testing denken, sollten wir es nicht als lästige Pflicht betrachten, sondern als kraftvolles Werkzeug, das uns dabei hilft, Vertrauen in unseren Code zu entwickeln und gleichzeitig die Entwicklungsgeschwindigkeit zu erhöhen. NestJS macht Testing zu einem integralen Bestandteil des Entwicklungsprozesses und bietet uns dabei ausgezeichnete Werkzeuge und Patterns.
Bevor wir uns in die technischen Details vertiefen, sollten wir verstehen, warum Testing in modernen Anwendungen so wichtig ist. Stellen Sie sich vor, Sie bauen ein Haus. Würden Sie das Fundament legen, ohne zu prüfen, ob es stabil ist? Würden Sie die Elektrik installieren, ohne zu testen, ob der Strom fließt? Genauso verhält es sich mit Software. Tests sind unser Werkzeug, um sicherzustellen, dass jeder Teil unserer Anwendung korrekt funktioniert.

Die Testing-Pyramide ist ein bewährtes Konzept, das uns dabei hilft, eine ausgewogene Test-Strategie zu entwickeln. Stellen Sie sich eine Pyramide vor, die aus drei Schichten besteht:
An der Basis finden wir Unit-Tests. Diese sind schnell, isoliert und testen einzelne Funktionen oder Klassen. Sie bilden das Fundament unserer Test-Suite und sollten den größten Anteil ausmachen. Denken Sie an Unit-Tests wie an die Überprüfung einzelner Bausteine vor dem Zusammenbau.
In der Mitte befinden sich Integration Tests. Diese testen das Zusammenspiel mehrerer Komponenten. Sie sind langsamer als Unit-Tests, aber sie geben uns Vertrauen, dass verschiedene Teile unserer Anwendung korrekt zusammenarbeiten.
An der Spitze stehen End-to-End Tests (E2E). Diese testen die gesamte Anwendung aus Benutzersicht. Sie sind am langsamsten, aber sie geben uns die höchste Sicherheit, dass die Anwendung als Ganzes funktioniert.
NestJS fördert eine Test-First-Mentalität. Das Framework ist so gestaltet, dass jede Komponente testbar ist. Dependency Injection, Modularität und klare Trennungen machen es einfach, Tests zu schreiben. Wenn Sie feststellen, dass eine Komponente schwer zu testen ist, ist das oft ein Zeichen dafür, dass das Design verbessert werden könnte.
NestJS nutzt Jest als Standard-Testing-Framework, ergänzt durch Supertest für HTTP-Tests. Diese Kombination bietet uns alles, was wir für umfassende Tests benötigen.
Jest ist nicht nur ein Test-Runner, sondern ein komplettes Testing-Ökosystem. Es bietet uns Test-Runner, Assertion-Library, Mocking-Funktionen und Code-Coverage-Reports in einem Paket. Denken Sie an Jest als Ihren Testing-Schweizer Taschenmesser.
// jest.config.js - Grundkonfiguration für NestJS
module.exports = {
// Verwende die NestJS Jest-Presets für optimale Konfiguration
preset: '@nestjs/testing',
// Definiere welche Dateien als Tests erkannt werden
testMatch: [
'**/__tests__/**/*.(test|spec).ts',
'**/*.(test|spec).ts'
],
// Sammle Coverage-Informationen von diesen Dateien
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.interface.ts',
'!src/main.ts',
],
// Coverage-Schwellenwerte definieren
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
// Test-Umgebung konfigurieren
testEnvironment: 'node',
// Setup-Dateien für Tests
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
// Module-Pfade für einfachere Imports
moduleNameMapping: {
'^src/(.*)$': '<rootDir>/src/$1',
},
};Supertest ist unser Werkzeug für das Testen von HTTP-Endpunkten. Es ermöglicht uns, HTTP-Requests zu simulieren und die Responses zu validieren. Stellen Sie sich Supertest als einen sehr geduldigen Benutzer vor, der systematisch alle Endpunkte Ihrer API testet.
// Beispiel für einen einfachen Supertest
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
describe('API Integration Tests', () => {
let app: INestApplication;
beforeAll(async () => {
// Hier erstellen wir eine Test-Instanz unserer Anwendung
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
// Wichtig: Ressourcen nach Tests aufräumen
await app.close();
});
it('should respond with health check', () => {
// Mit Supertest senden wir einen GET-Request und prüfen die Antwort
return request(app.getHttpServer())
.get('/health')
.expect(200)
.expect((response) => {
expect(response.body).toHaveProperty('status', 'ok');
});
});
});Das TestingModule ist das Herzstück des NestJS-Testing-Frameworks. Es ermöglicht uns, isolierte Test-Umgebungen zu erstellen, in denen wir Dependencies mocken und Komponenten unabhängig testen können.
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
describe('UsersService', () => {
let service: UsersService;
let repository: Repository<User>;
beforeEach(async () => {
// Erstelle ein isoliertes Testing-Modul
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
// Mocke das Repository für isolierte Tests
provide: getRepositoryToken(User),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
},
},
],
}).compile();
// Extrahiere die Services aus dem Test-Modul
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
// Hier würden unsere Tests folgen...
});Lassen Sie uns die verschiedenen Arten von Tests betrachten und verstehen, wann und wie wir sie einsetzen.
Unit-Tests sind die Grundbausteine unserer Test-Suite. Sie testen einzelne Funktionen oder Methoden isoliert von ihren Dependencies. Denken Sie an Unit-Tests wie an die Überprüfung einzelner Zahnräder einer Uhr, bevor Sie sie zusammenbauen.
import { UsersService } from './users.service';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { NotFoundException, ConflictException } from '@nestjs/common';
describe('UsersService Unit Tests', () => {
let service: UsersService;
let mockRepository: jest.Mocked<Repository<User>>;
beforeEach(async () => {
// Erstelle Mock-Implementierungen für alle Repository-Methoden
mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
describe('findById', () => {
it('should return a user when found', async () => {
// Arrange: Bereite Test-Daten vor
const userId = '123';
const expectedUser = {
id: userId,
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
};
// Konfiguriere das Mock-Verhalten
mockRepository.findOne.mockResolvedValue(expectedUser);
// Act: Führe die zu testende Aktion aus
const result = await service.findById(userId);
// Assert: Überprüfe das Ergebnis
expect(result).toEqual(expectedUser);
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
});
it('should throw NotFoundException when user not found', async () => {
// Arrange: Simuliere, dass kein User gefunden wird
const userId = 'non-existing-id';
mockRepository.findOne.mockResolvedValue(null);
// Act & Assert: Überprüfe, dass die richtige Exception geworfen wird
await expect(service.findById(userId)).rejects.toThrow(NotFoundException);
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
});
});
describe('create', () => {
it('should successfully create a new user', async () => {
// Arrange
const createUserDto = {
email: 'new@example.com',
firstName: 'Jane',
lastName: 'Smith',
password: 'securePassword123',
};
const createdUser = {
id: '456',
...createUserDto,
createdAt: new Date(),
updatedAt: new Date(),
};
// Simuliere, dass die E-Mail noch nicht existiert
mockRepository.findOne.mockResolvedValue(null);
mockRepository.create.mockReturnValue(createdUser);
mockRepository.save.mockResolvedValue(createdUser);
// Act
const result = await service.create(createUserDto);
// Assert
expect(result).toEqual(createdUser);
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { email: createUserDto.email }
});
expect(mockRepository.create).toHaveBeenCalledWith(createUserDto);
expect(mockRepository.save).toHaveBeenCalledWith(createdUser);
});
it('should throw ConflictException when email already exists', async () => {
// Arrange
const createUserDto = {
email: 'existing@example.com',
firstName: 'Jane',
lastName: 'Smith',
password: 'securePassword123',
};
const existingUser = { id: '789', email: createUserDto.email };
mockRepository.findOne.mockResolvedValue(existingUser);
// Act & Assert
await expect(service.create(createUserDto)).rejects.toThrow(ConflictException);
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { email: createUserDto.email }
});
// Stelle sicher, dass save nicht aufgerufen wird
expect(mockRepository.save).not.toHaveBeenCalled();
});
});
});Integration Tests überprüfen das Zusammenspiel mehrerer Komponenten. Sie sind besonders wertvoll, um sicherzustellen, dass verschiedene Services korrekt miteinander kommunizieren.
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
describe('Users Integration Tests', () => {
let module: TestingModule;
let service: UsersService;
let repository: Repository<User>;
beforeAll(async () => {
// Erstelle ein Test-Modul mit echter Datenbank-Verbindung
module = await Test.createTestingModule({
imports: [
// Verwende SQLite In-Memory-Datenbank für Tests
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
logging: false,
}),
UsersModule,
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
afterAll(async () => {
await module.close();
});
beforeEach(async () => {
// Räume die Datenbank vor jedem Test auf
await repository.clear();
});
describe('User CRUD Operations', () => {
it('should create, find, update and delete a user', async () => {
// Create - Erstelle einen neuen User
const createDto = {
email: 'integration@test.com',
firstName: 'Integration',
lastName: 'Test',
password: 'password123',
};
const createdUser = await service.create(createDto);
expect(createdUser).toBeDefined();
expect(createdUser.email).toBe(createDto.email);
// Read - Finde den erstellten User
const foundUser = await service.findById(createdUser.id);
expect(foundUser).toBeDefined();
expect(foundUser.email).toBe(createDto.email);
// Update - Aktualisiere den User
const updateDto = { firstName: 'Updated' };
const updatedUser = await service.update(createdUser.id, updateDto);
expect(updatedUser.firstName).toBe('Updated');
// Delete - Lösche den User
await service.remove(createdUser.id);
// Verifiziere, dass der User gelöscht wurde
await expect(service.findById(createdUser.id)).rejects.toThrow();
});
it('should handle concurrent user creation correctly', async () => {
// Teste, wie sich die Anwendung bei gleichzeitigen Operationen verhält
const createDto1 = {
email: 'concurrent1@test.com',
firstName: 'User',
lastName: 'One',
password: 'password123',
};
const createDto2 = {
email: 'concurrent2@test.com',
firstName: 'User',
lastName: 'Two',
password: 'password123',
};
// Führe beide Operationen gleichzeitig aus
const [user1, user2] = await Promise.all([
service.create(createDto1),
service.create(createDto2),
]);
expect(user1.email).toBe(createDto1.email);
expect(user2.email).toBe(createDto2.email);
expect(user1.id).not.toBe(user2.id);
});
});
});End-to-End Tests simulieren echte Benutzerinteraktionen und testen die gesamte Anwendung von Anfang bis Ende. Sie geben uns das höchste Vertrauen, dass unsere Anwendung korrekt funktioniert.
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TypeOrmModule } from '@nestjs/typeorm';
describe('Users E2E Tests', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
// Überschreibe die Datenbank-Konfiguration für Tests
.overrideModule(TypeOrmModule)
.useModule(
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
autoLoadEntities: true,
synchronize: true,
})
)
.compile();
app = moduleFixture.createNestApplication();
// Konfiguriere die Anwendung wie in der echten Umgebung
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new TransformInterceptor());
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('Authentication Flow', () => {
it('should register a new user and login', async () => {
const registerDto = {
email: 'e2e@test.com',
firstName: 'E2E',
lastName: 'Test',
password: 'strongPassword123!',
};
// Schritt 1: Registriere einen neuen User
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send(registerDto)
.expect(201);
expect(registerResponse.body).toHaveProperty('id');
expect(registerResponse.body.email).toBe(registerDto.email);
// Schritt 2: Logge den User ein
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: registerDto.email,
password: registerDto.password,
})
.expect(200);
expect(loginResponse.body).toHaveProperty('accessToken');
authToken = loginResponse.body.accessToken;
// Schritt 3: Zugriff auf geschützte Route
const profileResponse = await request(app.getHttpServer())
.get('/auth/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(profileResponse.body.email).toBe(registerDto.email);
});
it('should reject invalid credentials', async () => {
await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'nonexistent@test.com',
password: 'wrongPassword',
})
.expect(401);
});
});
describe('User Management', () => {
beforeEach(async () => {
// Stelle sicher, dass wir für jeden Test authentifiziert sind
if (!authToken) {
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'e2e@test.com',
password: 'strongPassword123!',
});
authToken = loginResponse.body.accessToken;
}
});
it('should get user profile', async () => {
const response = await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('email');
expect(response.body).not.toHaveProperty('password');
});
it('should update user profile', async () => {
const updateDto = {
firstName: 'Updated',
lastName: 'Name',
};
const response = await request(app.getHttpServer())
.patch('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.send(updateDto)
.expect(200);
expect(response.body.firstName).toBe(updateDto.firstName);
expect(response.body.lastName).toBe(updateDto.lastName);
});
it('should validate input data', async () => {
const invalidDto = {
email: 'invalid-email', // Ungültige E-Mail
firstName: '', // Leerer Name
};
const response = await request(app.getHttpServer())
.patch('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.send(invalidDto)
.expect(400);
expect(response.body).toHaveProperty('message');
expect(Array.isArray(response.body.message)).toBe(true);
});
});
});Controller sind die Eingangspunkte unserer API und verdienen besondere Aufmerksamkeit beim Testen. Wir müssen sicherstellen, dass sie HTTP-Requests korrekt verarbeiten und angemessen antworten.
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { NotFoundException } from '@nestjs/common';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
// Mock für den UsersService
const mockUsersService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
afterEach(() => {
// Räume die Mocks nach jedem Test auf
jest.clearAllMocks();
});
describe('create', () => {
it('should successfully create a user', async () => {
// Arrange
const createUserDto: CreateUserDto = {
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
password: 'password123',
};
const expectedResult = {
id: '1',
...createUserDto,
createdAt: new Date(),
updatedAt: new Date(),
};
mockUsersService.create.mockResolvedValue(expectedResult);
// Act
const result = await controller.create(createUserDto);
// Assert
expect(result).toEqual(expectedResult);
expect(service.create).toHaveBeenCalledWith(createUserDto);
expect(service.create).toHaveBeenCalledTimes(1);
});
it('should handle service errors appropriately', async () => {
// Arrange
const createUserDto: CreateUserDto = {
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
password: 'password123',
};
mockUsersService.create.mockRejectedValue(new Error('Database error'));
// Act & Assert
await expect(controller.create(createUserDto)).rejects.toThrow('Database error');
expect(service.create).toHaveBeenCalledWith(createUserDto);
});
});
describe('findAll', () => {
it('should return an array of users', async () => {
// Arrange
const expectedUsers = [
{ id: '1', email: 'user1@test.com', firstName: 'User', lastName: 'One' },
{ id: '2', email: 'user2@test.com', firstName: 'User', lastName: 'Two' },
];
mockUsersService.findAll.mockResolvedValue(expectedUsers);
// Act
const result = await controller.findAll();
// Assert
expect(result).toEqual(expectedUsers);
expect(service.findAll).toHaveBeenCalledTimes(1);
});
});
describe('findOne', () => {
it('should return a user by id', async () => {
// Arrange
const userId = '1';
const expectedUser = {
id: userId,
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
};
mockUsersService.findOne.mockResolvedValue(expectedUser);
// Act
const result = await controller.findOne(userId);
// Assert
expect(result).toEqual(expectedUser);
expect(service.findOne).toHaveBeenCalledWith(userId);
});
it('should throw NotFoundException when user does not exist', async () => {
// Arrange
const userId = 'non-existent';
mockUsersService.findOne.mockRejectedValue(new NotFoundException('User not found'));
// Act & Assert
await expect(controller.findOne(userId)).rejects.toThrow(NotFoundException);
expect(service.findOne).toHaveBeenCalledWith(userId);
});
});
describe('update', () => {
it('should update and return the user', async () => {
// Arrange
const userId = '1';
const updateUserDto: UpdateUserDto = {
firstName: 'Updated',
lastName: 'User',
};
const expectedResult = {
id: userId,
email: 'test@example.com',
...updateUserDto,
updatedAt: new Date(),
};
mockUsersService.update.mockResolvedValue(expectedResult);
// Act
const result = await controller.update(userId, updateUserDto);
// Assert
expect(result).toEqual(expectedResult);
expect(service.update).toHaveBeenCalledWith(userId, updateUserDto);
});
});
describe('remove', () => {
it('should delete a user', async () => {
// Arrange
const userId = '1';
mockUsersService.remove.mockResolvedValue(undefined);
// Act
await controller.remove(userId);
// Assert
expect(service.remove).toHaveBeenCalledWith(userId);
});
});
});Das Mocken von Services ist eine Kunst für sich. Wir müssen verstehen, welche Abhängigkeiten wir isolieren wollen und wie wir realistische Mock-Verhalten erstellen.
// Erweiterte Service-Mocking-Strategien
describe('UsersController with Advanced Mocking', () => {
let controller: UsersController;
let usersService: jest.Mocked<UsersService>;
let emailService: jest.Mocked<EmailService>;
beforeEach(async () => {
// Erstelle typisierte Mocks für bessere IntelliSense
const mockUsersService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
findByEmail: jest.fn(),
} as jest.Mocked<UsersService>;
const mockEmailService = {
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
} as jest.Mocked<EmailService>;
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{ provide: UsersService, useValue: mockUsersService },
{ provide: EmailService, useValue: mockEmailService },
],
}).compile();
controller = module.get<UsersController>(UsersController);
usersService = module.get(UsersService);
emailService = module.get(EmailService);
});
describe('create with email notification', () => {
it('should create user and send welcome email', async () => {
// Arrange
const createUserDto: CreateUserDto = {
email: 'new@example.com',
firstName: 'New',
lastName: 'User',
password: 'password123',
};
const createdUser = { id: '1', ...createUserDto };
usersService.create.mockResolvedValue(createdUser);
emailService.sendWelcomeEmail.mockResolvedValue(undefined);
// Act
const result = await controller.create(createUserDto);
// Assert
expect(result).toEqual(createdUser);
expect(usersService.create).toHaveBeenCalledWith(createUserDto);
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(createdUser.email, createdUser.firstName);
});
it('should create user even if email sending fails', async () => {
// Arrange
const createUserDto: CreateUserDto = {
email: 'new@example.com',
firstName: 'New',
lastName: 'User',
password: 'password123',
};
const createdUser = { id: '1', ...createUserDto };
usersService.create.mockResolvedValue(createdUser);
emailService.sendWelcomeEmail.mockRejectedValue(new Error('Email service unavailable'));
// Act
const result = await controller.create(createUserDto);
// Assert
expect(result).toEqual(createdUser);
expect(usersService.create).toHaveBeenCalledWith(createUserDto);
expect(emailService.sendWelcomeEmail).toHaveBeenCalled();
});
});
});Realistische Request/Response-Tests helfen uns sicherzustellen, dass unsere API korrekt mit HTTP-Protokoll umgeht.
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { ValidationPipe } from '@nestjs/common';
describe('Users API (Request/Response)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(UsersService)
.useValue({
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
})
.compile();
app = moduleRef.createNestApplication();
// Konfiguriere die App wie in Production
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('POST /users', () => {
it('should create a user with valid data', async () => {
const createUserDto = {
email: 'valid@example.com',
firstName: 'Valid',
lastName: 'User',
password: 'strongPassword123!',
};
const mockCreatedUser = { id: '1', ...createUserDto };
const usersService = app.get(UsersService);
jest.spyOn(usersService, 'create').mockResolvedValue(mockCreatedUser);
const response = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201);
expect(response.body).toMatchObject({
id: '1',
email: createUserDto.email,
firstName: createUserDto.firstName,
lastName: createUserDto.lastName,
});
// Passwort sollte nicht in der Response enthalten sein
expect(response.body).not.toHaveProperty('password');
});
it('should reject invalid email format', async () => {
const invalidDto = {
email: 'invalid-email', // Falsche E-Mail-Format
firstName: 'Valid',
lastName: 'User',
password: 'strongPassword123!',
};
const response = await request(app.getHttpServer())
.post('/users')
.send(invalidDto)
.expect(400);
expect(response.body).toHaveProperty('message');
expect(response.body.message).toContain('email');
});
it('should reject weak passwords', async () => {
const weakPasswordDto = {
email: 'valid@example.com',
firstName: 'Valid',
lastName: 'User',
password: '123', // Zu schwaches Passwort
};
const response = await request(app.getHttpServer())
.post('/users')
.send(weakPasswordDto)
.expect(400);
expect(response.body.message).toContain('password');
});
it('should handle content-type properly', async () => {
const createUserDto = {
email: 'valid@example.com',
firstName: 'Valid',
lastName: 'User',
password: 'strongPassword123!',
};
// Test mit falschem Content-Type
await request(app.getHttpServer())
.post('/users')
.set('Content-Type', 'text/plain')
.send(JSON.stringify(createUserDto))
.expect(400);
// Test mit korrektem Content-Type
await request(app.getHttpServer())
.post('/users')
.set('Content-Type', 'application/json')
.send(createUserDto)
.expect(201);
});
});
describe('GET /users/:id', () => {
it('should return user with correct headers', async () => {
const userId = '1';
const mockUser = {
id: userId,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
};
const usersService = app.get(UsersService);
jest.spyOn(usersService, 'findOne').mockResolvedValue(mockUser);
const response = await request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(200);
expect(response.body).toEqual(mockUser);
expect(response.headers['content-type']).toMatch(/application\/json/);
});
it('should return 404 for non-existent user', async () => {
const userId = 'non-existent';
const usersService = app.get(UsersService);
jest.spyOn(usersService, 'findOne').mockRejectedValue(new NotFoundException('User not found'));
const response = await request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(404);
expect(response.body).toHaveProperty('message', 'User not found');
});
});
});Services enthalten die Geschäftslogik unserer Anwendung und sind daher besonders wichtig zu testen. Sie sind oft komplex und haben viele Abhängigkeiten.
import { Test, TestingModule } from '@nestjs/testing';
import { OrdersService } from './orders.service';
import { Order } from './entities/order.entity';
import { User } from '../users/entities/user.entity';
import { Product } from '../products/entities/product.entity';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PaymentService } from '../payment/payment.service';
import { EmailService } from '../email/email.service';
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
describe('OrdersService Business Logic', () => {
let service: OrdersService;
let orderRepository: jest.Mocked<Repository<Order>>;
let paymentService: jest.Mocked<PaymentService>;
let emailService: jest.Mocked<EmailService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
OrdersService,
{
provide: getRepositoryToken(Order),
useValue: {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
createQueryBuilder: jest.fn(),
},
},
{
provide: PaymentService,
useValue: {
processPayment: jest.fn(),
refundPayment: jest.fn(),
},
},
{
provide: EmailService,
useValue: {
sendOrderConfirmation: jest.fn(),
sendOrderCancellation: jest.fn(),
},
},
],
}).compile();
service = module.get<OrdersService>(OrdersService);
orderRepository = module.get(getRepositoryToken(Order));
paymentService = module.get(PaymentService);
emailService = module.get(EmailService);
});
describe('createOrder', () => {
it('should create order with valid items and process payment', async () => {
// Arrange
const user: User = {
id: '1',
email: 'customer@example.com',
firstName: 'John',
lastName: 'Doe',
} as User;
const orderItems = [
{ productId: '1', quantity: 2, price: 29.99 },
{ productId: '2', quantity: 1, price: 49.99 },
];
const expectedTotal = (29.99 * 2) + (49.99 * 1); // 109.97
const mockOrder = {
id: 'order-1',
user,
items: orderItems,
total: expectedTotal,
status: 'confirmed',
} as Order;
// Mock-Verhalten konfigurieren
orderRepository.create.mockReturnValue(mockOrder);
orderRepository.save.mockResolvedValue(mockOrder);
paymentService.processPayment.mockResolvedValue({
success: true,
transactionId: 'txn-123',
});
emailService.sendOrderConfirmation.mockResolvedValue(undefined);
// Act
const result = await service.createOrder(user, orderItems);
// Assert
expect(result).toEqual(mockOrder);
expect(orderRepository.create).toHaveBeenCalledWith({
user,
items: orderItems,
total: expectedTotal,
status: 'pending',
});
expect(paymentService.processPayment).toHaveBeenCalledWith({
amount: expectedTotal,
currency: 'EUR',
customerId: user.id,
});
expect(emailService.sendOrderConfirmation).toHaveBeenCalledWith(
user.email,
mockOrder
);
});
it('should handle payment failure correctly', async () => {
// Arrange
const user: User = { id: '1', email: 'customer@example.com' } as User;
const orderItems = [{ productId: '1', quantity: 1, price: 29.99 }];
const mockOrder = {
id: 'order-1',
user,
items: orderItems,
total: 29.99,
status: 'pending',
} as Order;
orderRepository.create.mockReturnValue(mockOrder);
orderRepository.save.mockResolvedValue(mockOrder);
// Simuliere Zahlungsfehler
paymentService.processPayment.mockResolvedValue({
success: false,
error: 'Insufficient funds',
});
// Act & Assert
await expect(service.createOrder(user, orderItems)).rejects.toThrow(BadRequestException);
// Überprüfe, dass die Bestellung als fehlgeschlagen markiert wird
expect(orderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({ status: 'failed' })
);
// E-Mail sollte nicht gesendet werden bei Zahlungsfehler
expect(emailService.sendOrderConfirmation).not.toHaveBeenCalled();
});
it('should validate order items before processing', async () => {
// Arrange
const user: User = { id: '1', email: 'customer@example.com' } as User;
// Leere Bestellliste
const emptyOrderItems = [];
// Act & Assert
await expect(service.createOrder(user, emptyOrderItems)).rejects.toThrow(BadRequestException);
expect(orderRepository.create).not.toHaveBeenCalled();
expect(paymentService.processPayment).not.toHaveBeenCalled();
});
it('should calculate total correctly with discounts', async () => {
// Arrange
const user: User = {
id: '1',
email: 'premium@example.com',
membershipType: 'premium', // Premium-Kunde bekommt 10% Rabatt
} as User;
const orderItems = [
{ productId: '1', quantity: 2, price: 50.00 },
];
const subtotal = 100.00;
const discountAmount = 10.00; // 10% Rabatt
const expectedTotal = 90.00;
const mockOrder = {
id: 'order-1',
user,
items: orderItems,
subtotal,
discountAmount,
total: expectedTotal,
status: 'confirmed',
} as Order;
orderRepository.create.mockReturnValue(mockOrder);
orderRepository.save.mockResolvedValue(mockOrder);
paymentService.processPayment.mockResolvedValue({
success: true,
transactionId: 'txn-123',
});
// Act
const result = await service.createOrder(user, orderItems);
// Assert
expect(result.total).toBe(expectedTotal);
expect(result.discountAmount).toBe(discountAmount);
expect(paymentService.processPayment).toHaveBeenCalledWith(
expect.objectContaining({ amount: expectedTotal })
);
});
});
describe('cancelOrder', () => {
it('should cancel order and process refund', async () => {
// Arrange
const orderId = 'order-1';
const mockOrder = {
id: orderId,
status: 'confirmed',
total: 99.99,
paymentTransactionId: 'txn-123',
user: { email: 'customer@example.com' },
} as Order;
orderRepository.findOne.mockResolvedValue(mockOrder);
orderRepository.save.mockResolvedValue({ ...mockOrder, status: 'cancelled' });
paymentService.refundPayment.mockResolvedValue({ success: true });
emailService.sendOrderCancellation.mockResolvedValue(undefined);
// Act
const result = await service.cancelOrder(orderId);
// Assert
expect(result.status).toBe('cancelled');
expect(paymentService.refundPayment).toHaveBeenCalledWith('txn-123', 99.99);
expect(emailService.sendOrderCancellation).toHaveBeenCalledWith(
'customer@example.com',
mockOrder
);
});
it('should not allow cancellation of already shipped orders', async () => {
// Arrange
const orderId = 'order-1';
const mockOrder = {
id: orderId,
status: 'shipped', // Bereits versandt
total: 99.99,
} as Order;
orderRepository.findOne.mockResolvedValue(mockOrder);
// Act & Assert
await expect(service.cancelOrder(orderId)).rejects.toThrow(BadRequestException);
expect(paymentService.refundPayment).not.toHaveBeenCalled();
});
});
});Das effektive Mocken von Dependencies ist entscheidend für isolierte Unit-Tests.
// Verschiedene Mocking-Strategien demonstrieren
describe('OrdersService with Complex Dependencies', () => {
let service: OrdersService;
// Verwende jest.createMockFromModule für automatische Mocks
const mockInventoryService = jest.createMockFromModule<InventoryService>('../inventory/inventory.service');
const mockNotificationService = jest.createMockFromModule<NotificationService>('../notification/notification.service');
beforeEach(async () => {
// Realistischere Mock-Implementierungen
const mockRepositoryFactory = () => ({
create: jest.fn((entity) => ({ id: 'generated-id', ...entity })),
save: jest.fn((entity) => Promise.resolve(entity)),
find: jest.fn(() => Promise.resolve([])),
findOne: jest.fn(),
createQueryBuilder: jest.fn(() => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn(() => Promise.resolve([])),
getOne: jest.fn(() => Promise.resolve(null)),
})),
});
const module: TestingModule = await Test.createTestingModule({
providers: [
OrdersService,
{
provide: getRepositoryToken(Order),
useFactory: mockRepositoryFactory,
},
{
provide: InventoryService,
useValue: {
checkAvailability: jest.fn(),
reserveItems: jest.fn(),
releaseReservation: jest.fn(),
},
},
{
provide: NotificationService,
useValue: {
sendOrderNotification: jest.fn(),
sendInventoryAlert: jest.fn(),
},
},
],
}).compile();
service = module.get<OrdersService>(OrdersService);
});
describe('createOrderWithInventoryCheck', () => {
it('should check inventory before creating order', async () => {
// Arrange
const orderItems = [
{ productId: '1', quantity: 5 },
{ productId: '2', quantity: 2 },
];
const inventoryService = service['inventoryService']; // Private property access für Test
// Mock verschiedene Szenarien
inventoryService.checkAvailability
.mockResolvedValueOnce({ available: true, stock: 10 }) // Produkt 1
.mockResolvedValueOnce({ available: true, stock: 3 }); // Produkt 2
inventoryService.reserveItems.mockResolvedValue({ success: true });
// Act
await service.createOrderWithInventoryCheck(orderItems);
// Assert
expect(inventoryService.checkAvailability).toHaveBeenCalledTimes(2);
expect(inventoryService.checkAvailability).toHaveBeenNthCalledWith(1, '1', 5);
expect(inventoryService.checkAvailability).toHaveBeenNthCalledWith(2, '2', 2);
expect(inventoryService.reserveItems).toHaveBeenCalledWith(orderItems);
});
it('should handle inventory shortage gracefully', async () => {
// Arrange
const orderItems = [{ productId: '1', quantity: 10 }];
const inventoryService = service['inventoryService'];
inventoryService.checkAvailability.mockResolvedValue({
available: false,
stock: 5,
message: 'Insufficient stock',
});
// Act & Assert
await expect(service.createOrderWithInventoryCheck(orderItems))
.rejects.toThrow('Insufficient stock');
expect(inventoryService.reserveItems).not.toHaveBeenCalled();
});
});
describe('Async operation testing', () => {
it('should handle concurrent order creation', async () => {
// Teste, wie der Service mit gleichzeitigen Anfragen umgeht
const orderPromises = Array.from({ length: 5 }, (_, i) =>
service.createOrder({ id: `user-${i}` } as User, [
{ productId: '1', quantity: 1, price: 10 }
])
);
const results = await Promise.allSettled(orderPromises);
// Alle Bestellungen sollten erfolgreich sein
results.forEach(result => {
expect(result.status).toBe('fulfilled');
});
});
it('should timeout long-running operations', async () => {
// Simuliere einen langsamen externen Service
const inventoryService = service['inventoryService'];
inventoryService.checkAvailability.mockImplementation(
() => new Promise(resolve => setTimeout(resolve, 10000)) // 10 Sekunden
);
// Der Service sollte nach einem Timeout abbrechen
await expect(
service.createOrderWithInventoryCheck([{ productId: '1', quantity: 1 }])
).rejects.toThrow('Operation timeout');
});
});
});Für realistische Tests benötigen wir oft auch Database-Mocking-Strategien.
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { Repository, Connection } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
describe('UsersService with Database Mocking', () => {
let service: UsersService;
let repository: Repository<User>;
let connection: Connection;
describe('With Mock Repository', () => {
beforeEach(async () => {
// Erstelle einen detaillierten Repository-Mock
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(() => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getMany: jest.fn(),
getOne: jest.fn(),
getManyAndCount: jest.fn(),
})),
manager: {
transaction: jest.fn((callback) => callback({
save: jest.fn(),
create: jest.fn(),
findOne: jest.fn(),
})),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
it('should handle complex queries with mocked query builder', async () => {
// Arrange
const mockUsers = [
{ id: '1', email: 'user1@test.com', isActive: true },
{ id: '2', email: 'user2@test.com', isActive: true },
];
const queryBuilder = repository.createQueryBuilder();
(queryBuilder.getMany as jest.Mock).mockResolvedValue(mockUsers);
(queryBuilder.getManyAndCount as jest.Mock).mockResolvedValue([mockUsers, 2]);
// Act
const result = await service.findActiveUsersWithPagination(1, 10);
// Assert
expect(result.users).toEqual(mockUsers);
expect(result.total).toBe(2);
expect(queryBuilder.where).toHaveBeenCalledWith('user.isActive = :isActive', { isActive: true });
});
it('should handle database transactions in mocked environment', async () => {
// Arrange
const userData = {
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
};
const profileData = {
bio: 'Test bio',
avatar: 'avatar.jpg',
};
// Mock transaction behavior
(repository.manager.transaction as jest.Mock).mockImplementation(
async (callback) => {
const manager = {
save: jest.fn().mockResolvedValue({ id: '1', ...userData }),
create: jest.fn().mockReturnValue(userData),
};
return callback(manager);
}
);
// Act
const result = await service.createUserWithProfile(userData, profileData);
// Assert
expect(repository.manager.transaction).toHaveBeenCalled();
expect(result).toBeDefined();
});
});
describe('With In-Memory Database', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
logging: false,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
connection = module.get<Connection>(Connection);
});
afterEach(async () => {
await connection.close();
});
beforeEach(async () => {
// Räume die Datenbank vor jedem Test auf
await repository.clear();
});
it('should perform real database operations', async () => {
// Arrange
const userData = {
email: 'real@example.com',
firstName: 'Real',
lastName: 'User',
password: 'hashedPassword',
};
// Act
const createdUser = await service.create(userData);
const foundUser = await service.findById(createdUser.id);
// Assert
expect(foundUser).toBeDefined();
expect(foundUser.email).toBe(userData.email);
expect(foundUser.id).toBe(createdUser.id);
});
it('should handle database constraints', async () => {
// Arrange
const userData = {
email: 'duplicate@example.com',
firstName: 'First',
lastName: 'User',
password: 'password',
};
// Act - Erstelle ersten User
await service.create(userData);
// Assert - Versuche zweiten User mit gleicher E-Mail zu erstellen
await expect(service.create(userData)).rejects.toThrow();
});
});
});End-to-End-Tests mit Jest und Supertest bilden das Rückgrat unserer API-Testing-Strategie. Sie simulieren echte Benutzerinteraktionen und geben uns Vertrauen in die gesamte Anwendung.
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
describe('Complete E2E Application Flow', () => {
let app: INestApplication;
let authToken: string;
let userId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: '.env.test', // Spezielle Test-Umgebungsvariablen
}),
AppModule,
],
})
// Überschreibe die Produktions-Datenbank für Tests
.overrideModule(TypeOrmModule)
.useModule(
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
autoLoadEntities: true,
synchronize: true,
logging: false,
}),
)
.compile();
app = moduleFixture.createNestApplication();
// Konfiguriere die Anwendung genau wie in Production
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// Weitere Middleware wie in der echten Anwendung
app.enableCors();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('User Registration and Authentication Flow', () => {
const testUser = {
email: 'e2e.test@example.com',
firstName: 'E2E',
lastName: 'Test',
password: 'SecurePassword123!',
};
it('should complete full user registration flow', async () => {
// Schritt 1: User-Registrierung
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send(testUser)
.expect(201);
expect(registerResponse.body).toMatchObject({
email: testUser.email,
firstName: testUser.firstName,
lastName: testUser.lastName,
});
// Überprüfe, dass sensible Daten nicht zurückgegeben werden
expect(registerResponse.body).not.toHaveProperty('password');
userId = registerResponse.body.id;
// Schritt 2: Login mit neuen Credentials
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: testUser.email,
password: testUser.password,
})
.expect(200);
expect(loginResponse.body).toHaveProperty('accessToken');
expect(loginResponse.body).toHaveProperty('refreshToken');
expect(loginResponse.body).toHaveProperty('expiresIn');
authToken = loginResponse.body.accessToken;
// Schritt 3: Zugriff auf geschützte Route
const profileResponse = await request(app.getHttpServer())
.get('/auth/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(profileResponse.body.email).toBe(testUser.email);
});
it('should reject duplicate email registration', async () => {
// Versuche, den gleichen User nochmals zu registrieren
await request(app.getHttpServer())
.post('/auth/register')
.send(testUser)
.expect(409); // Conflict
});
it('should reject invalid login credentials', async () => {
await request(app.getHttpServer())
.post('/auth/login')
.send({
email: testUser.email,
password: 'WrongPassword',
})
.expect(401);
});
});
describe('Protected Resource Access', () => {
beforeEach(() => {
// Stelle sicher, dass wir für jeden Test authentifiziert sind
if (!authToken) {
throw new Error('Authentication required for this test suite');
}
});
it('should allow access to protected resources with valid token', async () => {
const response = await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('email');
expect(response.body).toHaveProperty('firstName');
});
it('should reject access without token', async () => {
await request(app.getHttpServer())
.get('/users/profile')
.expect(401);
});
it('should reject access with invalid token', async () => {
await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', 'Bearer invalid.token.here')
.expect(401);
});
it('should allow profile updates', async () => {
const updateData = {
firstName: 'Updated',
lastName: 'Name',
};
const response = await request(app.getHttpServer())
.patch('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.send(updateData)
.expect(200);
expect(response.body.firstName).toBe('Updated');
expect(response.body.lastName).toBe('Name');
// Verifiziere, dass die Änderungen persistent sind
const profileResponse = await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(profileResponse.body.firstName).toBe('Updated');
});
});
describe('Data Validation and Error Handling', () => {
it('should validate request data properly', async () => {
const invalidData = {
email: 'not-an-email',
firstName: '', // Empty string
lastName: 'ValidLastName',
password: '123', // Too short
};
const response = await request(app.getHttpServer())
.post('/auth/register')
.send(invalidData)
.expect(400);
expect(response.body).toHaveProperty('message');
expect(Array.isArray(response.body.message)).toBe(true);
// Überprüfe, dass spezifische Validierungsfehler enthalten sind
const messages = response.body.message.join(' ');
expect(messages).toContain('email');
expect(messages).toContain('firstName');
expect(messages).toContain('password');
});
it('should handle unexpected server errors gracefully', async () => {
// Simuliere einen Server-Fehler durch ungültige Daten
const response = await request(app.getHttpServer())
.get('/users/invalid-endpoint')
.set('Authorization', `Bearer ${authToken}`)
.expect(404);
expect(response.body).toHaveProperty('message');
expect(response.body).toHaveProperty('statusCode', 404);
});
});
describe('Rate Limiting and Security', () => {
it('should apply rate limiting to authentication endpoints', async () => {
const loginData = {
email: 'test@example.com',
password: 'wrongpassword',
};
// Führe viele fehlgeschlagene Login-Versuche durch
const promises = Array.from({ length: 10 }, () =>
request(app.getHttpServer())
.post('/auth/login')
.send(loginData)
);
const responses = await Promise.all(promises);
// Mindestens einer der Requests sollte rate-limited sein
const rateLimitedResponses = responses.filter(res => res.status === 429);
expect(rateLimitedResponses.length).toBeGreaterThan(0);
});
it('should set appropriate security headers', async () => {
const response = await request(app.getHttpServer())
.get('/health')
.expect(200);
// Überprüfe wichtige Security-Headers
expect(response.headers).toHaveProperty('x-content-type-options');
expect(response.headers).toHaveProperty('x-frame-options');
});
});
describe('API Performance and Reliability', () => {
it('should respond within acceptable time limits', async () => {
const startTime = Date.now();
await request(app.getHttpServer())
.get('/health')
.expect(200);
const responseTime = Date.now() - startTime;
// API sollte innerhalb von 1 Sekunde antworten
expect(responseTime).toBeLessThan(1000);
});
it('should handle concurrent requests properly', async () => {
// Führe mehrere gleichzeitige Requests aus
const concurrentRequests = Array.from({ length: 5 }, () =>
request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
);
const responses = await Promise.all(concurrentRequests);
// Alle Requests sollten erfolgreich sein
responses.forEach(response => {
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('email');
});
});
});
});Cypress bietet uns eine mächtige Alternative für End-to-End-Tests, besonders für Frontend-Integration und vollständige User-Journey-Tests.
// cypress/integration/user-management.spec.ts
describe('User Management E2E', () => {
beforeEach(() => {
// Setze die Test-Datenbank zurück
cy.task('db:seed');
// Besuche die Anwendung
cy.visit('/');
});
describe('User Registration Flow', () => {
it('should register a new user successfully', () => {
// Navigate to registration page
cy.get('[data-cy=register-link]').click();
// Fill out registration form
cy.get('[data-cy=email-input]').type('cypress@test.com');
cy.get('[data-cy=firstname-input]').type('Cypress');
cy.get('[data-cy=lastname-input]').type('Test');
cy.get('[data-cy=password-input]').type('SecurePassword123!');
cy.get('[data-cy=confirm-password-input]').type('SecurePassword123!');
// Submit form
cy.get('[data-cy=register-button]').click();
// Verify successful registration
cy.url().should('include', '/dashboard');
cy.get('[data-cy=welcome-message]').should('contain', 'Welcome, Cypress');
// Verify API call was made
cy.get('@registerRequest').should('have.property', 'response.statusCode', 201);
});
it('should show validation errors for invalid input', () => {
cy.get('[data-cy=register-link]').click();
// Submit form without filling fields
cy.get('[data-cy=register-button]').click();
// Check validation messages
cy.get('[data-cy=email-error]').should('be.visible');
cy.get('[data-cy=firstname-error]').should('be.visible');
cy.get('[data-cy=password-error]').should('be.visible');
});
it('should handle server errors gracefully', () => {
// Mock server error
cy.intercept('POST', '/api/auth/register', {
statusCode: 500,
body: { message: 'Internal server error' }
}).as('registerError');
cy.get('[data-cy=register-link]').click();
// Fill valid data
cy.get('[data-cy=email-input]').type('test@error.com');
cy.get('[data-cy=firstname-input]').type('Test');
cy.get('[data-cy=lastname-input]').type('User');
cy.get('[data-cy=password-input]').type('Password123!');
cy.get('[data-cy=confirm-password-input]').type('Password123!');
cy.get('[data-cy=register-button]').click();
// Verify error handling
cy.get('[data-cy=error-message]').should('contain', 'Internal server error');
cy.url().should('include', '/register'); // Should stay on registration page
});
});
describe('Authentication Flow', () => {
it('should login and access protected pages', () => {
// Use custom command for login
cy.login('existing@user.com', 'password123');
// Verify we're logged in
cy.url().should('include', '/dashboard');
cy.get('[data-cy=user-menu]').should('be.visible');
// Access protected page
cy.get('[data-cy=profile-link]').click();
cy.url().should('include', '/profile');
// Verify profile data is loaded
cy.get('[data-cy=profile-email]').should('contain', 'existing@user.com');
});
it('should logout successfully', () => {
cy.login('existing@user.com', 'password123');
// Logout
cy.get('[data-cy=user-menu]').click();
cy.get('[data-cy=logout-button]').click();
// Verify logout
cy.url().should('include', '/login');
cy.get('[data-cy=user-menu]').should('not.exist');
// Try to access protected page
cy.visit('/profile');
cy.url().should('include', '/login'); // Should redirect to login
});
it('should handle session expiration', () => {
cy.login('existing@user.com', 'password123');
// Mock expired token
cy.window().then(win => {
win.localStorage.setItem('token', 'expired.token.here');
});
// Try to access protected endpoint
cy.visit('/profile');
// Should redirect to login due to expired token
cy.url().should('include', '/login');
cy.get('[data-cy=session-expired-message]').should('be.visible');
});
});
describe('Profile Management', () => {
beforeEach(() => {
cy.login('existing@user.com', 'password123');
cy.visit('/profile');
});
it('should update profile information', () => {
// Update first name
cy.get('[data-cy=firstname-input]').clear().type('Updated');
cy.get('[data-cy=lastname-input]').clear().type('User');
cy.get('[data-cy=save-profile-button]').click();
// Verify success message
cy.get('[data-cy=success-message]').should('contain', 'Profile updated successfully');
// Verify changes are saved
cy.reload();
cy.get('[data-cy=firstname-input]').should('have.value', 'Updated');
cy.get('[data-cy=lastname-input]').should('have.value', 'User');
});
it('should change password', () => {
cy.get('[data-cy=change-password-tab]').click();
cy.get('[data-cy=current-password-input]').type('password123');
cy.get('[data-cy=new-password-input]').type('NewPassword123!');
cy.get('[data-cy=confirm-new-password-input]').type('NewPassword123!');
cy.get('[data-cy=change-password-button]').click();
// Verify success
cy.get('[data-cy=password-changed-message]').should('be.visible');
// Test login with new password
cy.logout();
cy.login('existing@user.com', 'NewPassword123!');
cy.url().should('include', '/dashboard');
});
it('should upload profile picture', () => {
// Mock file upload
const fileName = 'profile-picture.jpg';
cy.fixture(fileName).then(fileContent => {
cy.get('[data-cy=profile-picture-input]').selectFile({
contents: Cypress.Buffer.from(fileContent),
fileName,
mimeType: 'image/jpeg',
});
});
cy.get('[data-cy=upload-picture-button]').click();
// Verify upload success
cy.get('[data-cy=profile-picture]')
.should('have.attr', 'src')
.and('include', fileName);
});
});
});
// cypress/support/commands.ts - Custom Commands
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
logout(): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email,
password,
},
}).then((response) => {
window.localStorage.setItem('token', response.body.accessToken);
window.localStorage.setItem('refreshToken', response.body.refreshToken);
});
});
Cypress.Commands.add('logout', () => {
window.localStorage.removeItem('token');
window.localStorage.removeItem('refreshToken');
cy.visit('/login');
});
// cypress/plugins/index.ts - Database Seeding
module.exports = (on: any, config: any) => {
on('task', {
'db:seed': () => {
// Reset and seed test database
return require('../helpers/db-helper').seedDatabase();
},
});
};Playwright bietet moderne Browser-Automatisierung und ist besonders gut für komplexe User-Interactions.
// tests/e2e/user-journey.spec.ts
import { test, expect, Page } from '@playwright/test';
test.describe('Complete User Journey', () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
// Erstelle neuen Browser-Kontext für jeden Test
const context = await browser.newContext({
// Simuliere verschiedene Geräte/Browser
viewport: { width: 1280, height: 720 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
});
page = await context.newPage();
// Setze up API-Mocking
await page.route('/api/**', async route => {
const url = route.request().url();
if (url.includes('/auth/login')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
accessToken: 'mock.jwt.token',
refreshToken: 'mock.refresh.token',
user: { id: '1', email: 'test@example.com' }
})
});
} else {
await route.continue();
}
});
await page.goto('http://localhost:3000');
});
test('should complete user registration and login flow', async () => {
// Registration
await page.click('text=Register');
await page.fill('[data-testid=email-input]', 'playwright@test.com');
await page.fill('[data-testid=firstname-input]', 'Playwright');
await page.fill('[data-testid=lastname-input]', 'Test');
await page.fill('[data-testid=password-input]', 'SecurePassword123!');
await page.fill('[data-testid=confirm-password-input]', 'SecurePassword123!');
// Submit registration
await page.click('[data-testid=register-button]');
// Wait for navigation to dashboard
await page.waitForURL('**/dashboard');
await expect(page.locator('[data-testid=welcome-message]')).toContainText('Welcome');
});
test('should handle form validation errors', async () => {
await page.click('text=Register');
// Submit empty form
await page.click('[data-testid=register-button]');
// Check validation errors appear
await expect(page.locator('[data-testid=email-error]')).toBeVisible();
await expect(page.locator('[data-testid=password-error]')).toBeVisible();
// Fill valid email, invalid password
await page.fill('[data-testid=email-input]', 'valid@email.com');
await page.fill('[data-testid=password-input]', '123'); // Too short
await page.click('[data-testid=register-button]');
// Email error should be gone, password error remains
await expect(page.locator('[data-testid=email-error]')).toBeHidden();
await expect(page.locator('[data-testid=password-error]')).toBeVisible();
});
test('should work on mobile devices', async ({ browser }) => {
// Test mobile viewport
const mobileContext = await browser.newContext({
viewport: { width: 375, height: 667 }, // iPhone SE
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X)',
});
const mobilePage = await mobileContext.newPage();
await mobilePage.goto('http://localhost:3000');
// Mobile navigation should be collapsed
await expect(mobilePage.locator('[data-testid=mobile-menu-button]')).toBeVisible();
await expect(mobilePage.locator('[data-testid=desktop-navigation]')).toBeHidden();
// Test mobile menu functionality
await mobilePage.click('[data-testid=mobile-menu-button]');
await expect(mobilePage.locator('[data-testid=mobile-menu]')).toBeVisible();
await mobileContext.close();
});
test('should handle slow network conditions', async ({ browser }) => {
// Simuliere langsame Netzwerkverbindung
const context = await browser.newContext();
const cdpSession = await context.newCDPSession(page);
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 100 * 1024, // 100kb/s
uploadThroughput: 100 * 1024,
latency: 2000, // 2s latency
});
const slowPage = await context.newPage();
await slowPage.goto('http://localhost:3000');
// App sollte Loading-States anzeigen
await expect(slowPage.locator('[data-testid=loading-spinner]')).toBeVisible();
// Warte auf vollständiges Laden
await slowPage.waitForLoadState('networkidle');
await expect(slowPage.locator('[data-testid=loading-spinner]')).toBeHidden();
await context.close();
});
test('should be accessible', async () => {
// Teste Accessibility
await page.click('text=Register');
// Prüfe, dass Form-Labels korrekt mit Inputs verknüpft sind
const emailInput = page.locator('[data-testid=email-input]');
await expect(emailInput).toHaveAttribute('aria-label');
// Teste Keyboard-Navigation
await page.keyboard.press('Tab'); // Navigate to first field
await page.keyboard.type('test@example.com');
await page.keyboard.press('Tab'); // Navigate to next field
await page.keyboard.type('Test');
// Teste, dass Fehler-Messages screen reader zugänglich sind
await page.keyboard.press('Tab'); // Skip to submit button
await page.keyboard.press('Tab');
await page.keyboard.press('Enter'); // Submit incomplete form
const errorMessage = page.locator('[data-testid=password-error]');
await expect(errorMessage).toHaveAttribute('role', 'alert');
});
test('should handle API errors gracefully', async () => {
// Mock API error
await page.route('/api/auth/register', route =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal server error' })
})
);
await page.click('text=Register');
// Fill form with valid data
await page.fill('[data-testid=email-input]', 'test@example.com');
await page.fill('[data-testid=firstname-input]', 'Test');
await page.fill('[data-testid=lastname-input]', 'User');
await page.fill('[data-testid=password-input]', 'SecurePassword123!');
await page.fill('[data-testid=confirm-password-input]', 'SecurePassword123!');
await page.click('[data-testid=register-button]');
// Verify error message is displayed
await expect(page.locator('[data-testid=error-message]')).toContainText('server error');
// Verify user stays on registration page
await expect(page).toHaveURL(/.*register/);
});
test('should support offline functionality', async ({ browser }) => {
const context = await browser.newContext();
const offlinePage = await context.newPage();
// Go online first, load the app
await offlinePage.goto('http://localhost:3000');
await offlinePage.waitForLoadState('networkidle');
// Go offline
await context.setOffline(true);
// Test offline behavior
await offlinePage.reload();
await expect(offlinePage.locator('[data-testid=offline-banner]')).toBeVisible();
// Try to submit form while offline
await offlinePage.click('text=Register');
await offlinePage.fill('[data-testid=email-input]', 'offline@test.com');
await offlinePage.click('[data-testid=register-button]');
// Should show offline message
await expect(offlinePage.locator('[data-testid=offline-message]')).toBeVisible();
await context.close();
});
});
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
// Timeout für Tests
timeout: 30 * 1000,
// Erwarte, dass Tests nicht flaky sind
expect: {
timeout: 5000,
},
// Retry-Strategie
retries: process.env.CI ? 2 : 0,
// Parallel execution
workers: process.env.CI ? 1 : undefined,
// Reporter-Konfiguration
reporter: [
['html'],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
// Base URL für Tests
baseURL: 'http://localhost:3000',
// Screenshots bei Fehlern
screenshot: 'only-on-failure',
// Videos bei Fehlern
video: 'retain-on-failure',
// Trace-Aufzeichnung
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// Development Server
webServer: {
command: 'npm run start:test',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});Datenbankintegration in Tests erfordert besondere Aufmerksamkeit, da wir sowohl Performance als auch Isolation gewährleisten müssen.
// test/helpers/database-helper.ts
import { DataSource } from 'typeorm';
import { User } from '../../src/users/entities/user.entity';
import { Post } from '../../src/posts/entities/post.entity';
export class DatabaseTestHelper {
private static dataSource: DataSource;
static async setupTestDatabase(): Promise<DataSource> {
if (!this.dataSource) {
this.dataSource = new DataSource({
type: 'sqlite',
database: ':memory:', // In-Memory für schnelle Tests
entities: [User, Post],
synchronize: true,
logging: false,
dropSchema: true,
});
await this.dataSource.initialize();
}
return this.dataSource;
}
static async cleanDatabase(): Promise<void> {
if (!this.dataSource?.isInitialized) return;
// Lösche alle Daten in umgekehrter Reihenfolge der Dependencies
const entities = this.dataSource.entityMetadatas;
for (const entity of entities.reverse()) {
const repository = this.dataSource.getRepository(entity.name);
await repository.clear();
}
}
static async seedTestData(): Promise<{
users: User[];
posts: Post[];
}> {
const userRepository = this.dataSource.getRepository(User);
const postRepository = this.dataSource.getRepository(Post);
// Erstelle Test-User
const users = await userRepository.save([
{
email: 'admin@test.com',
firstName: 'Admin',
lastName: 'User',
password: 'hashedPassword',
role: 'admin',
},
{
email: 'user@test.com',
firstName: 'Regular',
lastName: 'User',
password: 'hashedPassword',
role: 'user',
},
]);
// Erstelle Test-Posts
const posts = await postRepository.save([
{
title: 'First Test Post',
content: 'This is the content of the first test post',
author: users[0],
published: true,
},
{
title: 'Second Test Post',
content: 'This is the content of the second test post',
author: users[1],
published: false,
},
]);
return { users, posts };
}
static async closeDatabase(): Promise<void> {
if (this.dataSource?.isInitialized) {
await this.dataSource.destroy();
}
}
}
// Integration Tests with Database
describe('Posts Service Database Integration', () => {
let dataSource: DataSource;
let postsService: PostsService;
let testData: { users: User[]; posts: Post[] };
beforeAll(async () => {
dataSource = await DatabaseTestHelper.setupTestDatabase();
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User, Post],
synchronize: true,
}),
TypeOrmModule.forFeature([User, Post]),
],
providers: [PostsService],
}).compile();
postsService = module.get<PostsService>(PostsService);
});
afterAll(async () => {
await DatabaseTestHelper.closeDatabase();
});
beforeEach(async () => {
await DatabaseTestHelper.cleanDatabase();
testData = await DatabaseTestHelper.seedTestData();
});
describe('findPublishedPosts', () => {
it('should return only published posts', async () => {
const publishedPosts = await postsService.findPublishedPosts();
expect(publishedPosts).toHaveLength(1);
expect(publishedPosts[0].title).toBe('First Test Post');
expect(publishedPosts[0].published).toBe(true);
});
it('should return posts with author information', async () => {
const publishedPosts = await postsService.findPublishedPosts();
expect(publishedPosts[0].author).toBeDefined();
expect(publishedPosts[0].author.email).toBe('admin@test.com');
});
});
describe('createPost', () => {
it('should create post with valid data', async () => {
const createPostDto = {
title: 'New Test Post',
content: 'Content for new test post',
published: true,
};
const newPost = await postsService.create(testData.users[0].id, createPostDto);
expect(newPost).toBeDefined();
expect(newPost.title).toBe(createPostDto.title);
expect(newPost.author.id).toBe(testData.users[0].id);
// Verifiziere, dass der Post in der Datenbank gespeichert wurde
const savedPost = await postsService.findById(newPost.id);
expect(savedPost.title).toBe(createPostDto.title);
});
it('should handle database constraints correctly', async () => {
// Versuche, einen Post mit ungültigem Author zu erstellen
const createPostDto = {
title: 'Invalid Post',
content: 'This should fail',
published: true,
};
await expect(
postsService.create('non-existent-user-id', createPostDto)
).rejects.toThrow();
});
});
describe('Complex Queries', () => {
it('should handle pagination correctly', async () => {
// Erstelle mehr Test-Daten
const postRepository = dataSource.getRepository(Post);
const additionalPosts = Array.from({ length: 15 }, (_, i) => ({
title: `Post ${i + 3}`,
content: `Content ${i + 3}`,
author: testData.users[0],
published: true,
}));
await postRepository.save(additionalPosts);
// Teste erste Seite
const page1 = await postsService.findPaginated(1, 5);
expect(page1.posts).toHaveLength(5);
expect(page1.total).toBe(16); // 1 original + 15 neue
expect(page1.currentPage).toBe(1);
expect(page1.totalPages).toBe(4);
// Teste zweite Seite
const page2 = await postsService.findPaginated(2, 5);
expect(page2.posts).toHaveLength(5);
expect(page2.currentPage).toBe(2);
// Überprüfe, dass keine Posts doppelt vorkommen
const page1Ids = page1.posts.map(p => p.id);
const page2Ids = page2.posts.map(p => p.id);
const intersection = page1Ids.filter(id => page2Ids.includes(id));
expect(intersection).toHaveLength(0);
});
it('should search posts by title and content', async () => {
const searchResults = await postsService.searchPosts('First');
expect(searchResults).toHaveLength(1);
expect(searchResults[0].title).toContain('First');
});
it('should handle concurrent database operations', async () => {
// Simuliere gleichzeitige Post-Erstellung
const createPromises = Array.from({ length: 5 }, (_, i) =>
postsService.create(testData.users[0].id, {
title: `Concurrent Post ${i}`,
content: `Content ${i}`,
published: true,
})
);
const createdPosts = await Promise.all(createPromises);
// Alle Posts sollten erfolgreich erstellt worden sein
expect(createdPosts).toHaveLength(5);
// Alle sollten unterschiedliche IDs haben
const ids = createdPosts.map(p => p.id);
const uniqueIds = [...new Set(ids)];
expect(uniqueIds).toHaveLength(5);
});
});
describe('Transaction Handling', () => {
it('should rollback on error in transaction', async () => {
const initialPostCount = await postsService.count();
try {
await dataSource.transaction(async manager => {
// Erstelle einen Post erfolgreich
const post = manager.create(Post, {
title: 'Transaction Test',
content: 'This should be rolled back',
author: testData.users[0],
published: true,
});
await manager.save(post);
// Simuliere einen Fehler
throw new Error('Simulated transaction error');
});
} catch (error) {
// Fehler ist erwartet
}
// Post-Anzahl sollte unverändert sein
const finalPostCount = await postsService.count();
expect(finalPostCount).toBe(initialPostCount);
});
});
});Für realistische Integration Tests können wir auch Docker-Container verwenden:
// test/helpers/docker-database.helper.ts
import { Client } from 'pg';
import { execSync } from 'child_process';
export class DockerDatabaseHelper {
private static containerId: string;
private static client: Client;
static async startPostgresContainer(): Promise<void> {
// Starte PostgreSQL Container
const command = `
docker run -d \
--name test-postgres-${Date.now()} \
-e POSTGRES_PASSWORD=testpass \
-e POSTGRES_USER=testuser \
-e POSTGRES_DB=testdb \
-p 0:5432 \
postgres:13-alpine
`;
this.containerId = execSync(command).toString().trim();
// Warte bis Container bereit ist
await this.waitForDatabase();
}
private static async waitForDatabase(): Promise<void> {
const port = this.getContainerPort();
this.client = new Client({
host: 'localhost',
port,
user: 'testuser',
password: 'testpass',
database: 'testdb',
});
let attempts = 0;
while (attempts < 30) {
try {
await this.client.connect();
console.log('Database ready');
return;
} catch (error) {
attempts++;
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
throw new Error('Database failed to start within timeout');
}
private static getContainerPort(): number {
const portCommand = `docker port ${this.containerId} 5432`;
const portOutput = execSync(portCommand).toString().trim();
return parseInt(portOutput.split(':')[1]);
}
static async stopContainer(): Promise<void> {
if (this.client) {
await this.client.end();
}
if (this.containerId) {
execSync(`docker stop ${this.containerId}`);
execSync(`docker rm ${this.containerId}`);
}
}
static getConnectionConfig() {
return {
host: 'localhost',
port: this.getContainerPort(),
username: 'testuser',
password: 'testpass',
database: 'testdb',
};
}
}
// Verwendung in Tests
describe('Posts with Real PostgreSQL', () => {
beforeAll(async () => {
await DockerDatabaseHelper.startPostgresContainer();
}, 60000); // Erhöhtes Timeout für Container-Start
afterAll(async () => {
await DockerDatabaseHelper.stopContainer();
});
// Tests hier...
});Guards, Interceptors und Pipes sind wichtige Middleware-Komponenten, die spezielle Testing-Strategien erfordern.
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
JwtAuthGuard,
{
provide: Reflector,
useValue: {
getAllAndOverride: jest.fn(),
},
},
],
}).compile();
guard = module.get<JwtAuthGuard>(JwtAuthGuard);
reflector = module.get<Reflector>(Reflector);
});
const createMockExecutionContext = (
headers: Record<string, string> = {},
user?: any
): ExecutionContext => {
const request = {
headers,
user,
};
return {
switchToHttp: () => ({
getRequest: () => request,
getResponse: () => ({}),
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as any;
};
describe('canActivate', () => {
it('should allow access for public routes', async () => {
// Simuliere @Public() Decorator
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(true);
const context = createMockExecutionContext();
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should deny access when no token is provided', async () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(false);
const context = createMockExecutionContext();
await expect(guard.canActivate(context)).rejects.toThrow('No token provided');
});
it('should allow access with valid token', async () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(false);
// Mock JWT verification
const mockUser = { id: '1', email: 'test@example.com' };
jest.spyOn(guard as any, 'validateToken').mockResolvedValue(mockUser);
const context = createMockExecutionContext({
authorization: 'Bearer valid.jwt.token',
});
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should deny access with invalid token', async () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(false);
jest.spyOn(guard as any, 'validateToken').mockRejectedValue(new Error('Invalid token'));
const context = createMockExecutionContext({
authorization: 'Bearer invalid.token',
});
await expect(guard.canActivate(context)).rejects.toThrow('Invalid token');
});
});
});
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RolesGuard,
{
provide: Reflector,
useValue: {
getAllAndOverride: jest.fn(),
},
},
],
}).compile();
guard = module.get<RolesGuard>(RolesGuard);
reflector = module.get<Reflector>(Reflector);
});
it('should allow access when no roles are required', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);
const context = createMockExecutionContext({}, { roles: ['user'] });
const result = guard.canActivate(context);
expect(result).toBe(true);
});
it('should allow access when user has required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = createMockExecutionContext({}, { roles: ['admin', 'user'] });
const result = guard.canActivate(context);
expect(result).toBe(true);
});
it('should deny access when user lacks required role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = createMockExecutionContext({}, { roles: ['user'] });
expect(() => guard.canActivate(context)).toThrow('Insufficient permissions');
});
const createMockExecutionContext = (
headers: Record<string, string> = {},
user?: any
): ExecutionContext => ({
switchToHttp: () => ({
getRequest: () => ({ headers, user }),
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as any);
});import { Test } from '@nestjs/testing';
import { CallHandler, ExecutionContext } from '@nestjs/common';
import { of, throwError } from 'rxjs';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
describe('LoggingInterceptor', () => {
let interceptor: LoggingInterceptor;
let mockLogger: any;
beforeEach(async () => {
mockLogger = {
log: jest.fn(),
error: jest.fn(),
};
const module = await Test.createTestingModule({
providers: [
LoggingInterceptor,
{ provide: 'Logger', useValue: mockLogger },
],
}).compile();
interceptor = module.get<LoggingInterceptor>(LoggingInterceptor);
});
const createMockExecutionContext = (
method: string = 'GET',
url: string = '/test'
): ExecutionContext => ({
switchToHttp: () => ({
getRequest: () => ({ method, url }),
}),
getHandler: () => ({ name: 'testHandler' }),
getClass: () => ({ name: 'TestController' }),
} as any);
const createMockCallHandler = (response?: any, shouldThrow?: boolean) => ({
handle: jest.fn(() =>
shouldThrow
? throwError(() => new Error('Test error'))
: of(response || { success: true })
),
});
it('should log request start and completion', async () => {
const context = createMockExecutionContext('GET', '/users');
const callHandler = createMockCallHandler({ users: [] });
const result = await interceptor
.intercept(context, callHandler)
.toPromise();
expect(mockLogger.log).toHaveBeenCalledWith('GET /users - Start');
expect(mockLogger.log).toHaveBeenCalledWith(
expect.stringContaining('GET /users - Success')
);
expect(result).toEqual({ users: [] });
});
it('should log errors', async () => {
const context = createMockExecutionContext('POST', '/users');
const callHandler = createMockCallHandler(null, true);
try {
await interceptor
.intercept(context, callHandler)
.toPromise();
} catch (error) {
// Error is expected
}
expect(mockLogger.log).toHaveBeenCalledWith('POST /users - Start');
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('POST /users - Error')
);
});
it('should measure response time', async () => {
const context = createMockExecutionContext();
const callHandler = createMockCallHandler();
// Mock Date.now to control timing
const originalDateNow = Date.now;
Date.now = jest.fn()
.mockReturnValueOnce(1000) // Start time
.mockReturnValueOnce(1500); // End time
await interceptor
.intercept(context, callHandler)
.toPromise();
expect(mockLogger.log).toHaveBeenCalledWith(
expect.stringContaining('Success (500ms)')
);
Date.now = originalDateNow;
});
});
describe('TransformInterceptor', () => {
let interceptor: TransformInterceptor;
beforeEach(() => {
interceptor = new TransformInterceptor();
});
it('should transform response data', async () => {
const context = {} as ExecutionContext;
const callHandler = {
handle: () => of({ id: 1, name: 'Test' }),
};
const result = await interceptor
.intercept(context, callHandler)
.toPromise();
expect(result).toEqual({
success: true,
data: { id: 1, name: 'Test' },
timestamp: expect.any(String),
});
});
it('should handle null responses', async () => {
const context = {} as ExecutionContext;
const callHandler = {
handle: () => of(null),
};
const result = await interceptor
.intercept(context, callHandler)
.toPromise();
expect(result).toEqual({
success: true,
data: null,
timestamp: expect.any(String),
});
});
it('should not transform error responses', async () => {
const context = {} as ExecutionContext;
const callHandler = {
handle: () => throwError(() => new Error('Test error')),
};
await expect(
interceptor.intercept(context, callHandler).toPromise()
).rejects.toThrow('Test error');
});
});import { Test } from '@nestjs/testing';
import { BadRequestException } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';
import { CustomValidationPipe } from './custom-validation.pipe';
import { CreateUserDto } from './dto/create-user.dto';
describe('ValidationPipe', () => {
let pipe: ValidationPipe;
beforeEach(() => {
pipe = new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
});
});
it('should validate and transform valid DTO', async () => {
const validDto = {
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123!',
};
const metadata = {
type: 'body',
metatype: CreateUserDto,
data: '',
};
const result = await pipe.transform(validDto, metadata);
expect(result).toBeInstanceOf(CreateUserDto);
expect(result.email).toBe(validDto.email);
});
it('should throw error for invalid DTO', async () => {
const invalidDto = {
email: 'invalid-email',
firstName: '', // Empty string
password: '123', // Too short
};
const metadata = {
type: 'body',
metatype: CreateUserDto,
data: '',
};
await expect(pipe.transform(invalidDto, metadata))
.rejects.toThrow(BadRequestException);
});
it('should remove non-whitelisted properties', async () => {
const dtoWithExtra = {
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123!',
maliciousField: 'should be removed',
};
const metadata = {
type: 'body',
metatype: CreateUserDto,
data: '',
};
const result = await pipe.transform(dtoWithExtra, metadata);
expect(result).not.toHaveProperty('maliciousField');
});
});
describe('ParseIntPipe', () => {
let pipe: ParseIntPipe;
beforeEach(() => {
pipe = new ParseIntPipe();
});
it('should parse valid string to integer', () => {
const result = pipe.transform('123', {
type: 'param',
metatype: Number,
data: 'id',
});
expect(result).toBe(123);
expect(typeof result).toBe('number');
});
it('should throw error for invalid string', () => {
expect(() =>
pipe.transform('not-a-number', {
type: 'param',
metatype: Number,
data: 'id',
})
).toThrow(BadRequestException);
});
it('should handle edge cases', () => {
// Test mit 0
expect(pipe.transform('0', {
type: 'param',
metatype: Number,
data: 'id',
})).toBe(0);
// Test mit negativen Zahlen
expect(pipe.transform('-42', {
type: 'param',
metatype: Number,
data: 'id',
})).toBe(-42);
});
});
describe('CustomValidationPipe', () => {
let pipe: CustomValidationPipe;
beforeEach(() => {
pipe = new CustomValidationPipe();
});
it('should sanitize HTML content', () => {
const dirtyData = {
content: '<script>alert("xss")</script>Safe content',
title: 'Clean title',
};
const result = pipe.transform(dirtyData, {
type: 'body',
metatype: Object,
data: '',
});
expect(result.content).toBe('Safe content');
expect(result.title).toBe('Clean title');
});
it('should normalize email addresses', () => {
const data = {
email: ' TEST@EXAMPLE.COM ',
};
const result = pipe.transform(data, {
type: 'body',
metatype: Object,
data: '',
});
expect(result.email).toBe('test@example.com');
});
it('should handle nested objects', () => {
const nestedData = {
user: {
profile: {
bio: '<b>Bold</b> content',
},
},
};
const result = pipe.transform(nestedData, {
type: 'body',
metatype: Object,
data: '',
});
expect(result.user.profile.bio).toBe('Bold content');
});
});Effektives Mocking ist essentiell für isolierte Unit-Tests und performante Test-Suites.
// Advanced Mocking Strategies
describe('Advanced Mocking Patterns', () => {
describe('Spy vs Mock vs Stub', () => {
let originalMethod: any;
let testObject: any;
beforeEach(() => {
testObject = {
getData: () => 'original data',
processData: (data: string) => `processed: ${data}`,
saveData: (data: string) => console.log(`saving: ${data}`),
};
originalMethod = testObject.getData;
});
afterEach(() => {
// Restore original method if needed
testObject.getData = originalMethod;
});
it('should use spy to monitor method calls', () => {
// Spy - überwacht Methodenaufrufe ohne das Verhalten zu ändern
const spy = jest.spyOn(testObject, 'getData');
const result = testObject.getData();
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(result).toBe('original data'); // Original behavior preserved
});
it('should use mock to replace method implementation', () => {
// Mock - ersetzt das Verhalten komplett
const mock = jest.spyOn(testObject, 'getData')
.mockReturnValue('mocked data');
const result = testObject.getData();
expect(mock).toHaveBeenCalled();
expect(result).toBe('mocked data'); // Behavior changed
});
it('should use stub for side-effect methods', () => {
// Stub - verhindert Seiteneffekte
const stub = jest.spyOn(testObject, 'saveData')
.mockImplementation(() => {}); // Do nothing
testObject.saveData('test data');
expect(stub).toHaveBeenCalledWith('test data');
// No console.log output because it's stubbed
});
});
describe('Complex Mock Scenarios', () => {
interface PaymentGateway {
processPayment(amount: number, cardToken: string): Promise<PaymentResult>;
refundPayment(transactionId: string): Promise<RefundResult>;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
errorCode?: string;
errorMessage?: string;
}
interface RefundResult {
success: boolean;
refundId?: string;
}
let mockPaymentGateway: jest.Mocked<PaymentGateway>;
let paymentService: PaymentService;
beforeEach(() => {
// Erstelle typisierte Mocks
mockPaymentGateway = {
processPayment: jest.fn(),
refundPayment: jest.fn(),
};
paymentService = new PaymentService(mockPaymentGateway);
});
it('should handle successful payment', async () => {
// Konfiguriere Mock für erfolgreiche Zahlung
mockPaymentGateway.processPayment.mockResolvedValue({
success: true,
transactionId: 'txn_123456',
});
const result = await paymentService.chargeCustomer(99.99, 'card_token');
expect(result.success).toBe(true);
expect(mockPaymentGateway.processPayment).toHaveBeenCalledWith(99.99, 'card_token');
});
it('should handle payment failures with retry', async () => {
// Erstes Mal fehlschlagen, zweites Mal erfolgreich
mockPaymentGateway.processPayment
.mockRejectedValueOnce(new Error('Network timeout'))
.mockResolvedValueOnce({
success: true,
transactionId: 'txn_retry_123',
});
const result = await paymentService.chargeCustomer(99.99, 'card_token');
expect(result.success).toBe(true);
expect(mockPaymentGateway.processPayment).toHaveBeenCalledTimes(2);
});
it('should handle payment gateway errors', async () => {
mockPaymentGateway.processPayment.mockResolvedValue({
success: false,
errorCode: 'INSUFFICIENT_FUNDS',
errorMessage: 'Card has insufficient funds',
});
const result = await paymentService.chargeCustomer(99.99, 'card_token');
expect(result.success).toBe(false);
expect(result.errorCode).toBe('INSUFFICIENT_FUNDS');
});
it('should track mock call history', async () => {
// Mehrere Zahlungen durchführen
await paymentService.chargeCustomer(25.00, 'card_1');
await paymentService.chargeCustomer(50.00, 'card_2');
await paymentService.chargeCustomer(75.00, 'card_1'); // Gleiche Karte
// Überprüfe alle Aufrufe
expect(mockPaymentGateway.processPayment).toHaveBeenCalledTimes(3);
expect(mockPaymentGateway.processPayment).toHaveBeenNthCalledWith(1, 25.00, 'card_1');
expect(mockPaymentGateway.processPayment).toHaveBeenNthCalledWith(2, 50.00, 'card_2');
expect(mockPaymentGateway.processPayment).toHaveBeenNthCalledWith(3, 75.00, 'card_1');
// Überprüfe, dass bestimmte Kombination aufgerufen wurde
expect(mockPaymentGateway.processPayment).toHaveBeenCalledWith(
expect.any(Number),
'card_1'
);
});
});
});
// Mock Factory Pattern
export class MockFactory {
static createMockRepository<T>(): jest.Mocked<Repository<T>> {
return {
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(() => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
leftJoin: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
getMany: jest.fn(),
getOne: jest.fn(),
getManyAndCount: jest.fn(),
getRawMany: jest.fn(),
getRawOne: jest.fn(),
})),
manager: {
transaction: jest.fn(),
save: jest.fn(),
create: jest.fn(),
findOne: jest.fn(),
},
} as any;
}
static createMockConfigService(config: Record<string, any> = {}): jest.Mocked<ConfigService> {
return {
get: jest.fn((key: string, defaultValue?: any) =>
config[key] !== undefined ? config[key] : defaultValue
),
getOrThrow: jest.fn((key: string) => {
if (config[key] === undefined) {
throw new Error(`Configuration key "${key}" not found`);
}
return config[key];
}),
} as any;
}
static createMockEmailService(): jest.Mocked<EmailService> {
return {
sendEmail: jest.fn().mockResolvedValue(undefined),
sendWelcomeEmail: jest.fn().mockResolvedValue(undefined),
sendPasswordResetEmail: jest.fn().mockResolvedValue(undefined),
sendNotificationEmail: jest.fn().mockResolvedValue(undefined),
} as any;
}
}
// Usage of Mock Factory
describe('Service with Mock Factory', () => {
let service: UsersService;
let mockRepository: jest.Mocked<Repository<User>>;
let mockEmailService: jest.Mocked<EmailService>;
let mockConfigService: jest.Mocked<ConfigService>;
beforeEach(async () => {
mockRepository = MockFactory.createMockRepository<User>();
mockEmailService = MockFactory.createMockEmailService();
mockConfigService = MockFactory.createMockConfigService({
'EMAIL_ENABLED': true,
'MAX_LOGIN_ATTEMPTS': 5,
});
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepository },
{ provide: EmailService, useValue: mockEmailService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should use factory-created mocks', async () => {
const userData = { email: 'test@example.com', firstName: 'Test' };
const savedUser = { id: '1', ...userData };
mockRepository.create.mockReturnValue(savedUser as User);
mockRepository.save.mockResolvedValue(savedUser as User);
const result = await service.create(userData as any);
expect(result).toEqual(savedUser);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
userData.email,
userData.firstName
);
});
});describe('Partial Mocking Strategies', () => {
describe('Smart Default Mocks', () => {
class SmartMockRepository {
private data: any[] = [];
private idCounter = 1;
// Implementiere realistische Standard-Verhalten
create(entityData: any) {
return { id: this.idCounter++, ...entityData };
}
async save(entity: any) {
if (entity.id) {
// Update existing
const index = this.data.findIndex(item => item.id === entity.id);
if (index >= 0) {
this.data[index] = { ...this.data[index], ...entity };
return this.data[index];
}
}
// Create new
const newEntity = { ...entity, id: entity.id || this.idCounter++ };
this.data.push(newEntity);
return newEntity;
}
async findOne(options: any) {
if (options.where) {
return this.data.find(item =>
Object.keys(options.where).every(key =>
item[key] === options.where[key]
)
) || null;
}
return this.data[0] || null;
}
async find(options: any = {}) {
let result = [...this.data];
if (options.where) {
result = result.filter(item =>
Object.keys(options.where).every(key =>
item[key] === options.where[key]
)
);
}
if (options.take) {
result = result.slice(0, options.take);
}
if (options.skip) {
result = result.slice(options.skip);
}
return result;
}
async remove(entity: any) {
const index = this.data.findIndex(item => item.id === entity.id);
if (index >= 0) {
this.data.splice(index, 1);
}
}
async clear() {
this.data = [];
this.idCounter = 1;
}
// Zusätzliche Hilfsmethoden für Tests
setTestData(data: any[]) {
this.data = data;
}
getTestData() {
return [...this.data];
}
}
let smartMock: SmartMockRepository;
let service: UsersService;
beforeEach(async () => {
smartMock = new SmartMockRepository();
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: smartMock },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should work with realistic mock behavior', async () => {
// Erstelle User
const user1 = await service.create({
email: 'user1@test.com',
firstName: 'User',
lastName: 'One',
});
expect(user1.id).toBeDefined();
// Finde User
const foundUser = await service.findById(user1.id);
expect(foundUser).toEqual(user1);
// Erstelle zweiten User
const user2 = await service.create({
email: 'user2@test.com',
firstName: 'User',
lastName: 'Two',
});
// Beide User sollten unterschiedliche IDs haben
expect(user2.id).not.toBe(user1.id);
// Liste alle User
const allUsers = await service.findAll();
expect(allUsers).toHaveLength(2);
});
it('should handle updates correctly', async () => {
// Seed initial data
smartMock.setTestData([
{ id: 1, email: 'existing@test.com', firstName: 'Existing' },
]);
const updatedUser = await service.update(1, { firstName: 'Updated' });
expect(updatedUser.firstName).toBe('Updated');
expect(updatedUser.email).toBe('existing@test.com'); // Unchanged
});
});
describe('Selective Mocking', () => {
let realUserService: UsersService;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(async () => {
// Nur bestimmte Dependencies mocken
mockEmailService = {
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
} as any;
const module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [
UsersService,
{ provide: EmailService, useValue: mockEmailService },
],
}).compile();
realUserService = module.get<UsersService>(UsersService);
});
it('should use real database but mock external services', async () => {
const userData = {
email: 'selective@test.com',
firstName: 'Selective',
lastName: 'Test',
password: 'password123',
};
// Nutze echte Datenbank für User-Operationen
const createdUser = await realUserService.create(userData);
// Aber mocke externe E-Mail-Service
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
userData.email,
userData.firstName
);
// Verifiziere, dass User wirklich in Datenbank gespeichert wurde
const foundUser = await realUserService.findById(createdUser.id);
expect(foundUser.email).toBe(userData.email);
});
});
});Code Coverage ist ein wichtiger Indikator für die Qualität unserer Test-Suite, sollte aber nicht als alleiniges Maß betrachtet werden.
// jest.config.js - Erweiterte Coverage-Konfiguration
module.exports = {
preset: '@nestjs/testing',
// Coverage-Sammlung
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.e2e-spec.ts',
'!src/**/*.interface.ts',
'!src/**/*.dto.ts',
'!src/**/*.entity.ts',
'!src/main.ts',
'!src/**/*.module.ts',
],
// Coverage-Reporter
coverageReporters: [
'text', // Console output
'text-summary', // Summary in console
'html', // HTML report
'lcov', // For CI/CD integration
'cobertura', // For some CI systems
'json', // Machine-readable format
],
// Coverage-Verzeichnisse
coverageDirectory: '<rootDir>/coverage',
// Coverage-Schwellenwerte
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
// Spezifische Schwellenwerte für kritische Module
'./src/auth/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
'./src/payment/': {
branches: 95,
functions: 95,
lines: 95,
statements: 95,
},
},
// Test-Umgebung
testEnvironment: 'node',
// Test-Muster
testMatch: [
'**/__tests__/**/*.(test|spec).ts',
'**/*.(test|spec).ts',
],
// Setup-Dateien
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
// Module-Mapping für einfachere Imports
moduleNameMapping: {
'^@/(.*): '<rootDir>/src/$1',
'^@test/(.*): '<rootDir>/test/$1',
},
// Ignoriere bestimmte Dateien für Coverage
coveragePathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/coverage/',
'.*\\.d\\.ts,
],
};// test/coverage-reporter.js
class CustomCoverageReporter {
constructor(globalConfig, options) {
this.globalConfig = globalConfig;
this.options = options;
}
onRunComplete(contexts, results) {
const { coverageMap } = results;
if (!coverageMap) {
console.log('No coverage data available');
return;
}
const summary = coverageMap.getCoverageSummary();
const metrics = {
statements: summary.statements.pct,
branches: summary.branches.pct,
functions: summary.functions.pct,
lines: summary.lines.pct,
};
// Erstelle detaillierte Berichte
this.generateDetailedReport(coverageMap);
this.checkQualityGates(metrics);
this.generateTrendReport(metrics);
}
generateDetailedReport(coverageMap) {
console.log('\n📊 Detailed Coverage Report:');
console.log('='.repeat(50));
coverageMap.files().forEach(file => {
const fileCoverage = coverageMap.fileCoverageFor(file);
const summary = fileCoverage.toSummary();
const relativePath = file.replace(process.cwd(), '');
// Farbige Ausgabe basierend auf Coverage
const getColor = (pct) => {
if (pct >= 80) return '\x1b[32m'; // Grün
if (pct >= 60) return '\x1b[33m'; // Gelb
return '\x1b[31m'; // Rot
};
const resetColor = '\x1b[0m';
console.log(
`${relativePath}: ` +
`${getColor(summary.lines.pct)}${summary.lines.pct.toFixed(1)}%${resetColor} lines, ` +
`${getColor(summary.branches.pct)}${summary.branches.pct.toFixed(1)}%${resetColor} branches`
);
});
}
checkQualityGates(metrics) {
console.log('\n🚦 Quality Gates:');
console.log('='.repeat(30));
const gates = [
{ name: 'Statements', value: metrics.statements, threshold: 80 },
{ name: 'Branches', value: metrics.branches, threshold: 80 },
{ name: 'Functions', value: metrics.functions, threshold: 80 },
{ name: 'Lines', value: metrics.lines, threshold: 80 },
];
let allPassed = true;
gates.forEach(gate => {
const passed = gate.value >= gate.threshold;
const status = passed ? '✅ PASS' : '❌ FAIL';
const color = passed ? '\x1b[32m' : '\x1b[31m';
console.log(
`${gate.name}: ${color}${gate.value.toFixed(1)}%\x1b[0m (threshold: ${gate.threshold}%) ${status}`
);
if (!passed) allPassed = false;
});
if (!allPassed) {
console.log('\n❌ Some quality gates failed!');
process.exit(1);
} else {
console.log('\n✅ All quality gates passed!');
}
}
generateTrendReport(currentMetrics) {
const fs = require('fs');
const path = require('path');
const trendsFile = path.join(process.cwd(), 'coverage-trends.json');
let trends = [];
// Lade vorherige Trends
if (fs.existsSync(trendsFile)) {
try {
trends = JSON.parse(fs.readFileSync(trendsFile, 'utf8'));
} catch (error) {
console.warn('Could not read coverage trends file');
}
}
// Füge aktuelle Metriken hinzu
trends.push({
timestamp: new Date().toISOString(),
commit: process.env.GIT_COMMIT || 'unknown',
...currentMetrics,
});
// Behalte nur die letzten 50 Einträge
if (trends.length > 50) {
trends = trends.slice(-50);
}
// Speichere Trends
fs.writeFileSync(trendsFile, JSON.stringify(trends, null, 2));
// Zeige Trend-Analyse
if (trends.length > 1) {
const previous = trends[trends.length - 2];
const current = trends[trends.length - 1];
console.log('\n📈 Coverage Trends:');
console.log('='.repeat(30));
Object.keys(currentMetrics).forEach(metric => {
const diff = current[metric] - previous[metric];
const arrow = diff > 0 ? '📈' : diff < 0 ? '📉' : '➡️';
const sign = diff > 0 ? '+' : '';
console.log(
`${metric}: ${current[metric].toFixed(1)}% (${sign}${diff.toFixed(1)}%) ${arrow}`
);
});
}
}
}
module.exports = CustomCoverageReporter;# .github/workflows/test-coverage.yml
name: Test Coverage
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Needed for trend analysis
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:cov
env:
GIT_COMMIT: ${{ github.sha }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Coverage Badge
uses: tj-actions/coverage-badge-action@v1
with:
coverage-file: coverage/coverage-summary.json
- name: Quality Gate Check
run: |
# Extrahiere Coverage-Metriken
COVERAGE=$(cat coverage/coverage-summary.json | jq -r '.total.lines.pct')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold of 80%"
exit 1
fi
echo "Coverage $COVERAGE% meets quality standards"
- name: Comment PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json'));
const comment = `
## 📊 Test Coverage Report
| Metric | Percentage | Status |
|--------|------------|--------|
| Statements | ${coverage.total.statements.pct}% | ${coverage.total.statements.pct >= 80 ? '✅' : '❌'} |
| Branches | ${coverage.total.branches.pct}% | ${coverage.total.branches.pct >= 80 ? '✅' : '❌'} |
| Functions | ${coverage.total.functions.pct}% | ${coverage.total.functions.pct >= 80 ? '✅' : '❌'} |
| Lines | ${coverage.total.lines.pct}% | ${coverage.total.lines.pct >= 80 ? '✅' : '❌'} |
[View detailed coverage report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});Lassen Sie uns mit den wichtigsten Best Practices abschließen, die Ihnen dabei helfen, eine robuste und wartbare Test-Suite aufzubauen.
// Beispiel für gut organisierte Test-Struktur
describe('UsersService', () => {
// Gruppiere zusammengehörige Tests
describe('User Creation', () => {
describe('with valid data', () => {
it('should create user with all required fields', () => {});
it('should hash password before saving', () => {});
it('should send welcome email', () => {});
it('should set default role to user', () => {});
});
describe('with invalid data', () => {
it('should reject duplicate email', () => {});
it('should reject weak password', () => {});
it('should reject invalid email format', () => {});
});
describe('with edge cases', () => {
it('should handle email with special characters', () => {});
it('should handle very long names', () => {});
it('should handle unicode characters', () => {});
});
});
describe('User Authentication', () => {
describe('successful login', () => {
it('should return JWT token', () => {});
it('should update last login timestamp', () => {});
it('should reset failed login attempts', () => {});
});
describe('failed login', () => {
it('should increment failed attempts counter', () => {});
it('should lock account after max attempts', () => {});
it('should log security event', () => {});
});
});
});// test/fixtures/user.fixtures.ts
export class UserFixtures {
static validUser() {
return {
email: 'valid@example.com',
firstName: 'John',
lastName: 'Doe',
password: 'SecurePassword123!',
};
}
static adminUser() {
return {
...this.validUser(),
email: 'admin@example.com',
role: 'admin',
};
}
static userWithWeakPassword() {
return {
...this.validUser(),
password: '123',
};
}
static userWithInvalidEmail() {
return {
...this.validUser(),
email: 'invalid-email',
};
}
static createUsersArray(count: number) {
return Array.from({ length: count }, (_, i) => ({
...this.validUser(),
email: `user${i}@example.com`,
firstName: `User${i}`,
}));
}
}
// Verwendung in Tests
describe('UsersService', () => {
it('should create user with valid data', async () => {
const userData = UserFixtures.validUser();
const result = await service.create(userData);
expect(result.email).toBe(userData.email);
});
});// test/helpers/performance.helper.ts
export class PerformanceTestHelper {
static measureExecutionTime<T>(fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
return new Promise(async (resolve) => {
const start = process.hrtime.bigint();
const result = await fn();
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000; // Convert to milliseconds
resolve({ result, duration });
});
}
static async expectExecutionTime<T>(
fn: () => Promise<T>,
maxDuration: number
): Promise<T> {
const { result, duration } = await this.measureExecutionTime(fn);
if (duration > maxDuration) {
throw new Error(
`Execution took ${duration}ms, expected less than ${maxDuration}ms`
);
}
return result;
}
}
// Verwendung in Tests
describe('Performance Tests', () => {
it('should find users within acceptable time', async () => {
await PerformanceTestHelper.expectExecutionTime(
() => usersService.findAll(),
100 // Max 100ms
);
});
it('should handle large datasets efficiently', async () => {
// Seed 1000 users
const users = UserFixtures.createUsersArray(1000);
await Promise.all(users.map(user => usersService.create(user)));
const { duration } = await PerformanceTestHelper.measureExecutionTime(
() => usersService.findPaginated(1, 50)
);
expect(duration).toBeLessThan(200); // Should complete within 200ms
});
});// Verwende Page Object Pattern für E2E-Tests
class LoginPage {
constructor(private page: any) {}
async navigateToLogin() {
await this.page.goto('/login');
}
async fillCredentials(email: string, password: string) {
await this.page.fill('[data-testid=email-input]', email);
await this.page.fill('[data-testid=password-input]', password);
}
async submitForm() {
await this.page.click('[data-testid=login-button]');
}
async getErrorMessage() {
return this.page.textContent('[data-testid=error-message]');
}
async login(email: string, password: string) {
await this.navigateToLogin();
await this.fillCredentials(email, password);
await this.submitForm();
}
}
// Verwendung in Tests
describe('Login Flow', () => {
let loginPage: LoginPage;
beforeEach(() => {
loginPage = new LoginPage(page);
});
it('should login successfully with valid credentials', async () => {
await loginPage.login('valid@example.com', 'password123');
await expect(page).toHaveURL(/dashboard/);
});
});Testing ist weit mehr als nur eine Notwendigkeit – es ist ein mächtiges Werkzeug, das uns dabei hilft, besseren Code zu schreiben und Vertrauen in unsere Anwendungen zu entwickeln. Mit den in diesem Kapitel vorgestellten Techniken und Patterns können Sie eine robuste, wartbare und effiziente Test-Suite für Ihre NestJS-Anwendungen aufbauen.
Denken Sie daran: Gute Tests sind eine Investition in die Zukunft Ihrer Anwendung. Sie sparen Zeit bei der Entwicklung, reduzieren Bugs in der Produktion und ermöglichen es Ihnen, mit Vertrauen zu refactoren und neue Features hinzuzufügen.