Testing NestJS Applications
Writing unit tests, integration tests, and end-to-end tests using Jest and Supertest.
Testing NestJS Exception Filters and Error Handling
This document provides a comprehensive guide on testing exception filters and error handling logic within the NestJS framework. Ensuring robust error handling is crucial for building reliable and user-friendly applications. This guide will cover the essential concepts and demonstrate practical testing techniques.
Introduction to NestJS Exception Filters
NestJS provides a powerful mechanism for handling unhandled exceptions through Exception Filters. Exception filters allow you to intercept exceptions thrown within your application and transform them into user-friendly responses. This centralizes error handling logic, promoting code reusability and maintainability.
Key Concepts:
@Catch()
decorator: Used to define the type of exception a filter handles. Can handle specific exceptions or the baseHttpException
.ExceptionFilter
interface: Requires the implementation of acatch(exception: any, host: ArgumentsHost)
method.ArgumentsHost
: Provides access to the underlying platform's (e.g., Express, Fastify) request and response objects.
Writing Unit Tests for Exception Filters
Testing your exception filters ensures they behave as expected under various error conditions. Here's a structured approach to writing effective unit tests:
1. Setting up the Testing Environment
Use Jest (or your preferred testing framework) and the NestJS testing module to create a controlled testing environment.
// app.module.ts (example)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// app.controller.ts (example)
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/error')
triggerError(): string {
throw new HttpException('Simulated Error', HttpStatus.BAD_REQUEST);
}
}
// src/exception-filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
// app.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HttpException, HttpStatus } from '@nestjs/common';
import { HttpExceptionFilter } from '../src/exception-filters/http-exception.filter';
import * as request from 'supertest';
import { AppModule } from './app.module';
describe('AppController (e2e)', () => {
let app;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalFilters(new HttpExceptionFilter()); // Apply the filter globally for testing
await app.init();
});
it('should handle HttpException and return the expected response', async () => {
return request(app.getHttpServer())
.get('/error')
.expect(HttpStatus.BAD_REQUEST)
.expect((res) => {
expect(res.body).toEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: 'Simulated Error',
path: '/error',
timestamp: expect.any(String), // Match any string, as timestamp will vary
});
});
});
});
2. Mocking Dependencies
Mock the ArgumentsHost
and any other dependencies used within the filter to isolate the unit under test. This is often done using Jest's mocking capabilities.
//See example above.
3. Asserting the Output
Verify that the filter correctly transforms the exception and sets the appropriate properties on the response object. Use Jest's assertion methods to check the status code, headers, and response body.
//See example above.
Testing Specific Error Scenarios
Consider testing a variety of error scenarios to ensure your filters are robust:
- Different HTTP status codes: Test with 400, 401, 403, 404, 500 errors.
- Custom exceptions: Create custom exception classes and test how the filter handles them.
- Exceptions with and without messages: Ensure the filter handles cases where the exception message is missing or empty.
- Validation errors: Test how the filter transforms validation errors (e.g., from
class-validator
) into appropriate responses.
Best Practices
- Keep filters focused: Each filter should handle a specific type of exception or a related group of exceptions.
- Write clear and concise tests: Tests should be easy to understand and maintain.
- Use descriptive test names: Test names should clearly indicate the scenario being tested.
- Cover all possible error scenarios: Think about all the ways your application can fail and write tests to handle those scenarios.
Example: Testing a Custom Validation Exception Filter
This example demonstrates testing a filter that handles validation errors.
// validation.exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { ValidationError } from 'class-validator';
import { ValidationException } from './validation.exception';
@Catch(ValidationException)
export class ValidationExceptionFilter implements ExceptionFilter {
catch(exception: ValidationException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = HttpStatus.BAD_REQUEST;
const errors = exception.validationErrors.map(err => ({
property: err.property,
constraints: err.constraints,
}));
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: 'Validation failed',
errors: errors,
});
}
}
// validation.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { ValidationError } from 'class-validator';
export class ValidationException extends HttpException {
public validationErrors: ValidationError[];
constructor(validationErrors: ValidationError[]) {
super('Validation failed', HttpStatus.BAD_REQUEST);
this.validationErrors = validationErrors;
}
}
// validation.exception.filter.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ValidationExceptionFilter } from './validation.exception.filter';
import { ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Response, Request } from 'express';
import { ValidationException } from './validation.exception';
import { ValidationError } from 'class-validator';
describe('ValidationExceptionFilter', () => {
let filter: ValidationExceptionFilter;
beforeEach(() => {
filter = new ValidationExceptionFilter();
});
it('should catch ValidationException and return the expected response', () => {
const mockValidationError: ValidationError = {
property: 'name',
constraints: { isNotEmpty: 'name should not be empty' },
} as any;
const mockValidationException = new ValidationException([mockValidationError]);
const mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
} as any as Response;
const mockRequest = {
url: '/test',
} as any as Request;
const mockArgumentsHost: ArgumentsHost = {
switchToHttp: () => ({
getResponse: () => mockResponse,
getRequest: () => mockRequest,
} as any),
getArgByIndex: jest.fn(),
getArgs: jest.fn(),
getType: jest.fn(),
};
filter.catch(mockValidationException, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST);
expect(mockResponse.json).toHaveBeenCalledWith({
statusCode: HttpStatus.BAD_REQUEST,
timestamp: expect.any(String),
path: '/test',
message: 'Validation failed',
errors: [{
property: 'name',
constraints: { isNotEmpty: 'name should not be empty' },
}],
});
});
});
Conclusion
Thoroughly testing your NestJS exception filters is vital for building robust and reliable applications. By following the guidelines and examples presented in this document, you can ensure that your application handles errors gracefully and provides meaningful feedback to the client.