NestJS Testing Guide: Unit Tests, Integration Tests & E2E with Jest
Let me be blunt: if you are shipping a NestJS app without tests, you are gambling with your users' trust. I have seen it happen -- a "quick refactor" breaks the Stripe webhook handler, and suddenly subscriptions are not renewing. A tenant isolation bug leaks data between organizations. These are not hypothetical scenarios. They are Tuesday. 🧪
The good news? NestJS makes testing genuinely enjoyable. Its dependency injection system was practically designed for mocking, and Jest integrates beautifully. Let me show you exactly how to test every layer of your application.
Why Testing Matters for NestJS Applications
NestJS applications tend to grow into complex systems with dozens of services, guards, interceptors, and database queries. Without tests, every change becomes a gamble. Will the Stripe webhook handler still work after you refactored the billing service? Will the tenant isolation guard still block cross-tenant access?
Tests answer these questions in seconds instead of hours of manual checking.
This guide covers three types of tests for NestJS: unit tests for isolated logic, integration tests for service-to-database interactions, and end-to-end tests for full HTTP request flows.
Setting Up Jest in NestJS
NestJS projects created with the CLI come with Jest pre-configured. Verify your setup:
npm run test # Run unit tests
npm run test:e2e # Run end-to-end tests
npm run test:cov # Run with coverage report
Your jest configuration lives in package.json or jest.config.ts. The defaults work for most projects.
Cloudrix SaaS Starter ships with this pre-configured and tested.
Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.
Try the live demo →Unit Testing Services
Unit tests verify a single class in isolation. All dependencies are mocked. Here is a TasksService with a repository dependency:
// tasks.service.ts
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private readonly taskRepo: Repository<Task>,
) {}
async findOne(id: string): Promise<Task> {
const task = await this.taskRepo.findOne({ where: { id } });
if (!task) {
throw new NotFoundException(`Task ${id} not found`);
}
return task;
}
async create(dto: CreateTaskDto, userId: string): Promise<Task> {
const task = this.taskRepo.create({ ...dto, ownerId: userId });
return this.taskRepo.save(task);
}
}
The test file mocks the repository using NestJS testing utilities:
// tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotFoundException } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { Task } from './task.entity';
describe('TasksService', () => {
let service: TasksService;
let repo: jest.Mocked<Repository<Task>>;
const mockRepo = {
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TasksService,
{ provide: getRepositoryToken(Task), useValue: mockRepo },
],
}).compile();
service = module.get(TasksService);
repo = module.get(getRepositoryToken(Task));
});
afterEach(() => jest.clearAllMocks());
describe('findOne', () => {
it('should return a task when found', async () => {
const task = { id: '1', title: 'Test' } as Task;
mockRepo.findOne.mockResolvedValue(task);
const result = await service.findOne('1');
expect(result).toEqual(task);
expect(mockRepo.findOne).toHaveBeenCalledWith({
where: { id: '1' },
});
});
it('should throw NotFoundException when not found', async () => {
mockRepo.findOne.mockResolvedValue(null);
await expect(service.findOne('999')).rejects.toThrow(
NotFoundException,
);
});
});
describe('create', () => {
it('should create and save a task', async () => {
const dto = { title: 'New', description: 'Test' };
const created = { ...dto, ownerId: 'user-1' } as Task;
mockRepo.create.mockReturnValue(created);
mockRepo.save.mockResolvedValue({ ...created, id: '1' });
const result = await service.create(dto, 'user-1');
expect(mockRepo.create).toHaveBeenCalledWith({
...dto,
ownerId: 'user-1',
});
expect(result.id).toBe('1');
});
});
});
The key pattern: use Test.createTestingModule to build a minimal module with mocked dependencies. Each test verifies one behavior.
Unit Testing Guards
Guards are critical for security. Always test them. This is especially important for RBAC implementations. Here is an RBAC guard and its test:
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// roles.guard.spec.ts
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(() => {
reflector = new Reflector();
guard = new RolesGuard(reflector);
});
it('should allow access when no roles are required', () => {
jest.spyOn(reflector, 'get').mockReturnValue(undefined);
const context = createMockContext({});
expect(guard.canActivate(context)).toBe(true);
});
it('should allow access when user has required role', () => {
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
const context = createMockContext({ roles: ['admin', 'user'] });
expect(guard.canActivate(context)).toBe(true);
});
it('should deny access when user lacks required role', () => {
jest.spyOn(reflector, 'get').mockReturnValue(['admin']);
const context = createMockContext({ roles: ['user'] });
expect(guard.canActivate(context)).toBe(false);
});
});
function createMockContext(user: any): ExecutionContext {
return {
getHandler: () => jest.fn(),
switchToHttp: () => ({
getRequest: () => ({ user }),
}),
} as any;
}
💡 Testing guards is non-negotiable. They are your security layer. A missed edge case here means unauthorized access. Learn more about building robust guards in our NestJS RBAC implementation guide.
Integration Testing with a Real Database
Integration tests verify that your services work correctly with an actual database. Use a test database that gets reset between runs:
// tasks.integration.spec.ts
describe('TasksService (integration)', () => {
let service: TasksService;
let module: TestingModule;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5433,
database: 'test_db',
entities: [Task],
synchronize: true,
dropSchema: true,
}),
TypeOrmModule.forFeature([Task]),
],
providers: [TasksService],
}).compile();
service = module.get(TasksService);
});
afterAll(async () => {
await module.close();
});
it('should persist and retrieve a task', async () => {
const created = await service.create(
{ title: 'Integration test', description: 'Verify DB' },
'user-1',
);
const found = await service.findOne(created.id);
expect(found.title).toBe('Integration test');
expect(found.ownerId).toBe('user-1');
});
it('should throw when task does not exist', async () => {
await expect(
service.findOne('nonexistent-id'),
).rejects.toThrow(NotFoundException);
});
});
The dropSchema: true and synchronize: true options ensure a clean database for each test run. Use a separate database or Docker container for test isolation.
End-to-End Testing
E2E tests send real HTTP requests to your running application. NestJS provides supertest integration out of the box:
// 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';
describe('Tasks API (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
it('POST /tasks - should create a task', () => {
return request(app.getHttpServer())
.post('/tasks')
.send({ title: 'E2E Test', description: 'Testing' })
.expect(201)
.expect((res) => {
expect(res.body.title).toBe('E2E Test');
expect(res.body.id).toBeDefined();
});
});
it('POST /tasks - should reject invalid input', () => {
return request(app.getHttpServer())
.post('/tasks')
.send({ title: '' })
.expect(400);
});
it('GET /tasks/:id - should return 404 for missing task', () => {
return request(app.getHttpServer())
.get('/tasks/nonexistent')
.expect(404);
});
});
E2E tests catch issues that unit tests miss: middleware ordering, validation pipe configuration, serialization behavior, and guard interactions.
🔥 Want to see comprehensive testing in action? The Cloudrix SaaS Starter ships with 55+ test files covering auth flows, tenant isolation, billing webhooks, and more. It is the best reference for real-world NestJS testing patterns. Try it free.
Testing Best Practices
Name tests by behavior, not implementation. Write "should throw when task not found" instead of "should call findOne and return null". Behavior-focused names survive refactoring.
Use factories for test data. Instead of creating objects inline, build a factory:
function buildTask(overrides: Partial<Task> = {}): Task {
return {
id: 'test-id',
title: 'Default Task',
description: 'Default description',
status: TaskStatus.OPEN,
ownerId: 'user-1',
createdAt: new Date(),
...overrides,
};
}
Test error paths, not just happy paths. Most bugs hide in error handling. Test what happens with null inputs, missing relations, duplicate entries, and unauthorized access.
Keep tests fast. Unit tests should run in under 5 seconds total. If they are slow, you are probably hitting a real database or making real HTTP calls. Mock those.
Test Coverage That Matters
Aim for coverage in the right places:
- Guards and interceptors: 100% coverage. These are your security layer.
- Services with business logic: 90%+ coverage. This is where bugs cost money.
- Controllers: 70%+ is fine. They are thin and mostly delegate to services.
- DTOs and entities: Skip coverage for plain data classes.
Real-World Scale 🎯
The Cloudrix SaaS Starter ships with 55+ test files covering authentication flows, tenant isolation, billing webhooks, RBAC guards, and API endpoints. Every pattern described in this guide is implemented and tested in a production-grade codebase.
If you want to see what comprehensive NestJS testing looks like in practice, explore the starter and use it as a reference for your own projects.
Stop shipping untested code. Start with the free Lite version, or get Pro for $249 one-time here. Your future self -- and your users -- will thank you. 🚀
Related guides: NestJS Testing with E2E patterns | NestJS REST API Tutorial | Docker Compose for NestJS