Testing is a critical aspect of software development that ensures code quality, prevents regressions, and gives developers confidence when making changes. NestJS provides excellent testing utilities that make it easy to write comprehensive tests for your applications. Let's explore how to effectively test NestJS applications at all levels.
Testing Fundamentals
NestJS applications can be tested at three levels: unit tests, integration tests, and end-to-end (E2E) tests. Each level serves a different purpose and provides unique value.
Testing Utilities
NestJS provides two main testing utilities:
- 1@nestjs/testing: For unit and integration testing
- 2Supertest: For E2E testing of HTTP endpoints
These utilities work seamlessly with popular testing frameworks like Jest.
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';Unit Testing
Unit tests focus on testing individual classes in isolation. In NestJS, this typically means testing services, controllers, and other providers.
Testing Services
Services contain the core business logic and should be thoroughly tested:
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { DatabaseService } from '../database/database.service';
import { ConflictException, NotFoundException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
let databaseService: DatabaseService;
const mockDatabaseService = {
query: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: DatabaseService,
useValue: mockDatabaseService,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
databaseService = module.get<DatabaseService>(DatabaseService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('findOne', () => {
it('should return a user when found', async () => {
const mockUser = {
id: '1',
username: 'johndoe',
email: 'john@example.com',
};
mockDatabaseService.findOne.mockResolvedValue(mockUser);
const result = await service.findOne('1');
expect(result).toEqual(mockUser);
expect(databaseService.findOne).toHaveBeenCalledWith({
where: { id: '1' },
});
});
it('should throw NotFoundException when user not found', async () => {
mockDatabaseService.findOne.mockResolvedValue(null);
await expect(service.findOne('999')).rejects.toThrow(
new NotFoundException('User not found'),
);
});
});
describe('create', () => {
it('should create a new user successfully', async () => {
const createUserDto = {
username: 'johndoe',
email: 'john@example.com',
password: 'password123',
};
const mockUser = {
id: '1',
...createUserDto,
};
mockDatabaseService.findOne.mockResolvedValue(null);
mockDatabaseService.insert.mockResolvedValue(mockUser);
const result = await service.create(createUserDto);
expect(result).toEqual(mockUser);
expect(databaseService.insert).toHaveBeenCalledWith({
data: expect.objectContaining({
username: createUserDto.username,
email: createUserDto.email,
}),
});
});
it('should throw ConflictException if username exists', async () => {
const createUserDto = {
username: 'johndoe',
email: 'john@example.com',
password: 'password123',
};
const existingUser = {
id: '1',
username: 'johndoe',
};
mockDatabaseService.findOne.mockResolvedValue(existingUser);
await expect(service.create(createUserDto)).rejects.toThrow(
new ConflictException('Username already exists'),
);
});
});
});Unit tests should be fast and isolated. Mock all external dependencies.
Testing Controllers
Controllers handle HTTP requests and delegate to services. Test their routing and error handling:
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { ConflictException, NotFoundException } from '@nestjs/common';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: 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(() => {
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return an array of users', async () => {
const mockUsers = [
{ id: '1', username: 'johndoe', email: 'john@example.com' },
{ id: '2', username: 'janedoe', email: 'jane@example.com' },
];
mockUsersService.findAll.mockResolvedValue(mockUsers);
const result = await controller.findAll();
expect(result).toEqual(mockUsers);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('create', () => {
it('should create and return a user', async () => {
const createUserDto = {
username: 'johndoe',
email: 'john@example.com',
password: 'password123',
};
const mockUser = {
id: '1',
...createUserDto,
};
mockUsersService.create.mockResolvedValue(mockUser);
const result = await controller.create(createUserDto);
expect(result).toEqual(mockUser);
expect(service.create).toHaveBeenCalledWith(createUserDto);
});
it('should throw ConflictException on duplicate username', async () => {
const createUserDto = {
username: 'johndoe',
email: 'john@example.com',
password: 'password123',
};
mockUsersService.create.mockRejectedValue(
new ConflictException('Username already exists'),
);
await expect(controller.create(createUserDto)).rejects.toThrow(
ConflictException,
);
});
});
});Controller tests verify HTTP status codes, response bodies, and error handling.
Integration Testing
Integration tests verify that multiple components work together correctly.
Testing Module Integration
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { UsersModule } from './users.module';
import { DatabaseModule } from '../database/database.module';
import { DatabaseService } from '../database/database.service';
describe('UsersModule Integration', () => {
let app: INestApplication;
let databaseService: DatabaseService;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [UsersModule, DatabaseModule],
})
.overrideProvider(DatabaseService)
.useValue({
query: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
})
.compile();
app = moduleFixture.createNestApplication();
await app.init();
databaseService = app.get<DatabaseService>(DatabaseService);
});
afterAll(async () => {
await app.close();
});
it('should be defined', () => {
expect(app).toBeDefined();
});
it('should resolve UsersService dependency', () => {
const usersService = app.get<UsersService>(UsersService);
expect(usersService).toBeDefined();
});
});Integration tests verify that dependency injection works and modules are properly configured.
End-to-End Testing
E2E tests test the entire application stack, from HTTP requests to database operations.
Setting Up E2E Tests
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (GET)', () => {
it('should return an array of users', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect(res => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
describe('/users/:id (GET)', () => {
it('should return a single user', () => {
return request(app.getHttpServer())
.get('/users/1')
.expect(200)
.expect(res => {
expect(res.body).toHaveProperty('id', '1');
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/999')
.expect(404);
});
});
describe('/users (POST)', () => {
it('should create a new user', () => {
const newUser = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
};
return request(app.getHttpServer())
.post('/users')
.send(newUser)
.expect(201)
.expect(res => {
expect(res.body).toHaveProperty('id');
expect(res.body.username).toBe(newUser.username);
});
});
it('should return 400 for invalid input', () => {
const invalidUser = {
username: 't',
email: 'not-an-email',
};
return request(app.getHttpServer())
.post('/users')
.send(invalidUser)
.expect(400);
});
});
});E2E tests verify the entire request/response cycle, including validation, authentication, and database operations.
Testing with Test Database
Use a separate test database for E2E tests:
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';
describe('Users E2E Tests', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
prisma = app.get<PrismaService>(PrismaService);
// Clean database before tests
await prisma.user.deleteMany({});
});
afterAll(async () => {
await prisma.user.deleteMany({});
await app.close();
});
beforeEach(async () => {
// Clean database before each test
await prisma.user.deleteMany({});
});
it('should create and retrieve a user', async () => {
const createUserDto = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
};
// Create user
const createResponse = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201);
const userId = createResponse.body.id;
// Retrieve user
await request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(200)
.expect(res => {
expect(res.body.id).toBe(userId);
expect(res.body.username).toBe(createUserDto.username);
});
});
});Using a test database ensures tests are isolated and repeatable.
Testing Guards and Interceptors
Test your custom guards and interceptors:
import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { Reflector } from '@nestjs/core';
@Injectable()
class MockAuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
request.user = { id: '1', username: 'testuser' };
return true;
}
}
describe('AuthGuard', () => {
let guard: AuthGuard;
let reflector: Reflector;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AuthGuard,
{
provide: Reflector,
useValue: {
getAllAndOverride: jest.fn(),
},
},
],
}).compile();
guard = module.get<AuthGuard>(AuthGuard);
reflector = module.get<Reflector>(Reflector);
});
it('should allow access with valid token', () => {
const context = createMockExecutionContext({
headers: {
authorization: 'Bearer valid-token',
},
});
expect(guard.canActivate(context)).toBe(true);
});
it('should deny access without token', () => {
const context = createMockExecutionContext({
headers: {},
});
expect(guard.canActivate(context)).toBe(false);
});
});
function createMockExecutionContext(request: any): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => request,
}),
} as any;
}Testing guards ensures authentication and authorization work correctly.
Testing Pipes
Test custom validation and transformation pipes:
import { ValidationPipe } from './validation.pipe';
describe('ValidationPipe', () => {
let pipe: ValidationPipe;
beforeEach(() => {
pipe = new ValidationPipe();
});
it('should transform and validate valid data', () => {
const metadata = {
type: 'body',
metatype: class CreateUserDto {
username: string;
email: string;
},
};
const value = {
username: 'testuser',
email: 'test@example.com',
};
const result = pipe.transform(value, metadata);
expect(result.username).toBe(value.username);
expect(result.email).toBe(value.email);
});
it('should throw BadRequestException for invalid data', () => {
const metadata = {
type: 'body',
metatype: class CreateUserDto {
username: string;
email: string;
},
};
const value = {
username: 't',
email: 'not-an-email',
};
expect(() => {
pipe.transform(value, metadata);
}).toThrow();
});
});Pipe tests ensure data transformation and validation work as expected.
Testing GraphQL Resolvers
Test GraphQL resolvers using NestJS testing utilities:
import { Test, TestingModule } from '@nestjs/testing';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
describe('UsersResolver', () => {
let resolver: UsersResolver;
let service: UsersService;
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersResolver,
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();
resolver = module.get<UsersResolver>(UsersResolver);
service = module.get<UsersService>(UsersService);
});
describe('users query', () => {
it('should return an array of users', async () => {
const mockUsers = [
{ id: '1', username: 'johndoe', email: 'john@example.com' },
];
mockUsersService.findAll.mockResolvedValue(mockUsers);
const result = await resolver.users();
expect(result).toEqual(mockUsers);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('createUser mutation', () => {
it('should create a new user', async () => {
const userInput = {
username: 'johndoe',
email: 'john@example.com',
password: 'password123',
};
const mockUser = {
id: '1',
...userInput,
};
mockUsersService.create.mockResolvedValue(mockUser);
const result = await resolver.createUser(
userInput.username,
userInput.email,
userInput.password,
);
expect(result).toEqual(mockUser);
expect(service.create).toHaveBeenCalledWith(userInput);
});
});
});GraphQL resolver tests verify queries, mutations, and field resolvers work correctly.
Best Practices
Follow these testing best practices:
- 1Test behavior, not implementation: Focus on what the code does, not how it does it
- 2Keep tests independent: Each test should work in isolation
- 3Use descriptive names: Test names should clearly describe what they test
- 4Arrange, Act, Assert: Structure tests clearly with these three phases
- 5Mock external dependencies: Don't make real API calls or database connections in unit tests
- 6Test edge cases: Test null, undefined, empty arrays, and error conditions
- 7Keep tests fast: Unit tests should run quickly
- 8Use test doubles: Mock, stub, or spy on dependencies
- 9Test at the right level: Don't test frameworks or libraries
- 10Maintain test code: Keep tests clean and refactored
Conclusion
Testing NestJS applications is straightforward with the provided utilities. By writing comprehensive unit, integration, and E2E tests, you ensure your application works correctly and can be confidently maintained and enhanced. Remember that testing is an investment that pays off in code quality, reduced bugs, and easier maintenance.
As your application grows, maintain a high test coverage and make testing part of your development workflow. The confidence that comes from a well-tested application is invaluable, especially when making changes or adding new features.