NestJS Testing: Unit, Integration, and E2E Testing Guide
NestJS comes with Jest pre-configured and a full testing module built into the framework. Most teams generate the boilerplate, delete the placeholder it('should be defined') tests, and ship anyway.
The testing infrastructure is already there. Here's how to actually use it.
NestJS Testing Architecture
NestJS testing revolves around @nestjs/testing, which lets you create a full application module in test context with dependency injection intact:
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});This is the starting point. It compiles your module and wires up the dependency injection tree exactly as in production.
Layer 1: Unit Testing Services
Services contain your business logic. Test them in isolation by mocking their dependencies.
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { User } from './user.entity';
const mockUsersRepository = {
find: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockUsersRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return an array of users', async () => {
const expectedUsers = [
{ id: 1, email: 'alice@test.com' },
{ id: 2, email: 'bob@test.com' },
];
mockUsersRepository.find.mockResolvedValue(expectedUsers);
const result = await service.findAll();
expect(result).toEqual(expectedUsers);
expect(mockUsersRepository.find).toHaveBeenCalledTimes(1);
});
});
describe('findOne', () => {
it('should return a user when found', async () => {
const user = { id: 1, email: 'alice@test.com' };
mockUsersRepository.findOneBy.mockResolvedValue(user);
const result = await service.findOne(1);
expect(result).toEqual(user);
});
it('should throw NotFoundException when user does not exist', async () => {
mockUsersRepository.findOneBy.mockResolvedValue(null);
await expect(service.findOne(99999)).rejects.toThrow('User not found');
});
});
describe('create', () => {
it('should hash password before saving', async () => {
const dto = { email: 'new@test.com', password: 'plaintext123' };
mockUsersRepository.save.mockResolvedValue({ id: 3, ...dto });
await service.create(dto);
const savedUser = mockUsersRepository.save.mock.calls[0][0];
expect(savedUser.password).not.toBe('plaintext123');
expect(savedUser.password).toMatch(/^\$2[ab]\$\d+\$/); // bcrypt hash pattern
});
});
});Layer 2: Testing Controllers
Controllers handle HTTP concerns. Test them with a mocked service.
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
const mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{ provide: UsersService, useValue: mockUsersService },
],
}).compile();
controller = module.get<UsersController>(UsersController);
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return users from service', async () => {
const users = [{ id: 1, email: 'alice@test.com' }];
mockUsersService.findAll.mockResolvedValue(users);
const result = await controller.findAll();
expect(result).toEqual(users);
});
});
describe('create', () => {
it('should call service with DTO', async () => {
const dto: CreateUserDto = { email: 'new@test.com', password: 'pass123' };
const created = { id: 5, email: 'new@test.com' };
mockUsersService.create.mockResolvedValue(created);
const result = await controller.create(dto);
expect(mockUsersService.create).toHaveBeenCalledWith(dto);
expect(result).toEqual(created);
});
});
});Layer 3: Testing Guards and Interceptors
Guards and interceptors are the hardest NestJS pieces to test. Use ExecutionContext mocks:
// auth/jwt-auth.guard.spec.ts
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtService } from '@nestjs/jwt';
import { ExecutionContext } from '@nestjs/common';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let jwtService: JwtService;
beforeEach(() => {
jwtService = { verify: jest.fn() } as any;
guard = new JwtAuthGuard(jwtService);
});
function createMockContext(token?: string): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => ({
headers: token ? { authorization: `Bearer ${token}` } : {},
}),
}),
} as ExecutionContext;
}
it('should allow requests with valid tokens', async () => {
jwtService.verify = jest.fn().mockReturnValue({ sub: 1, email: 'user@test.com' });
const ctx = createMockContext('valid.jwt.token');
const canActivate = await guard.canActivate(ctx);
expect(canActivate).toBe(true);
});
it('should reject requests without Authorization header', async () => {
const ctx = createMockContext();
await expect(guard.canActivate(ctx)).rejects.toThrow('Unauthorized');
});
it('should reject requests with expired tokens', async () => {
jwtService.verify = jest.fn().mockImplementation(() => {
throw new Error('jwt expired');
});
const ctx = createMockContext('expired.jwt.token');
await expect(guard.canActivate(ctx)).rejects.toThrow('Unauthorized');
});
});Layer 4: Testing Pipes and Validation
NestJS pipes transform and validate input. Test them directly:
// validation/parse-positive-int.pipe.spec.ts
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';
import { BadRequestException } from '@nestjs/common';
describe('ParsePositiveIntPipe', () => {
let pipe: ParsePositiveIntPipe;
beforeEach(() => {
pipe = new ParsePositiveIntPipe();
});
it('should parse valid positive integer strings', () => {
expect(pipe.transform('5')).toBe(5);
expect(pipe.transform('100')).toBe(100);
});
it('should throw BadRequestException for zero', () => {
expect(() => pipe.transform('0')).toThrow(BadRequestException);
});
it('should throw BadRequestException for negative numbers', () => {
expect(() => pipe.transform('-3')).toThrow(BadRequestException);
});
it('should throw BadRequestException for non-numeric strings', () => {
expect(() => pipe.transform('abc')).toThrow(BadRequestException);
expect(() => pipe.transform('')).toThrow(BadRequestException);
});
});Layer 5: E2E Tests with Supertest
E2E tests run the full NestJS application against a real HTTP server:
// test/app.e2e-spec.ts
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';
describe('Users (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(TypeOrmModule)
.useModule(TypeOrmModule.forRoot({ /* test DB config */ }))
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
// Set up auth token for protected route tests
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@test.com', password: 'pass123' });
authToken = loginResponse.body.access_token;
});
afterAll(async () => {
await app.close();
});
describe('GET /users', () => {
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users')
.expect(401);
});
it('should return users with valid token', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect(res => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
describe('POST /users', () => {
it('should create user with valid data', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'new@test.com', password: 'securepass123' })
.expect(201)
.expect(res => {
expect(res.body.email).toBe('new@test.com');
expect(res.body.password).toBeUndefined(); // Should not expose password
});
});
it('should return 400 with invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'not-an-email', password: 'pass123' })
.expect(400)
.expect(res => {
expect(res.body.message).toContain('email');
});
});
});
});Testing NestJS with a Database
Use an in-memory SQLite database for integration tests to keep them fast and isolated:
// test/database.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../src/users/user.entity';
export const TestDatabaseModule = TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
dropSchema: true,
});What Tests Miss
Your Jest suite proves your NestJS modules are wired correctly in isolation. It doesn't prove your deployed application handles real traffic.
Production NestJS failures:
- Environment variable gaps — a
ConfigServicekey is missing in production but present in test.env, causing silent undefined values - Middleware ordering — your global pipes and interceptors run in a different order than expected in a deployed cluster
- WebSocket connection drops — NestJS gateway logic works in test but fails under load in production
- Circular dependency warnings becoming errors — rare in test, surfaces in production at scale
Production Monitoring for NestJS APIs
HelpMeTest lets you run behavioral tests against your deployed NestJS service continuously:
Test: user registration flow
POST /users with email and password
Status is 201
Body contains id and email
GET /users/:id returns the created user
Response time under 500msTests run on a schedule. If your guards stop working after a JWT library update, your validation pipe silently passes bad data, or an endpoint returns 500 under load, you find out immediately.
Free tier: 10 tests, unlimited health checks. Try HelpMeTest →
NestJS Testing Checklist
- Service unit tests: every public method, error cases, dependency mocks
- Controller unit tests: routing to correct service method, response shape
- Guard unit tests: allow valid, reject invalid, reject missing credentials
- Pipe unit tests: transform valid inputs, throw on invalid inputs
- Interceptor tests: transforms response correctly, handles errors
- E2E tests with Supertest: full HTTP request/response cycles
- Validation pipe tested with invalid data at the HTTP level
- Database integration tests with test DB (not production)
- Production monitoring: behavioral tests after every deploy
The @nestjs/testing module is there. Use it.