NestJS Testing
Master testing in NestJS for building reliable applications with comprehensive unit, integration, and end-to-end tests.
Unit Testing Setup
Creating and configuring test modules with TestingModule.
import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from './entities/user.entity';
describe('UserService', () => { let service: UserService; let module: TestingModule;
beforeEach(async () => { module = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useValue: { find: jest.fn(), findOne: jest.fn(), save: jest.fn(), create: jest.fn(), delete: jest.fn(), }, }, ], }).compile();
service = module.get<UserService>(UserService);
});
afterEach(async () => { await module.close(); });
it('should be defined', () => { expect(service).toBeDefined(); });
it('should find all users', async () => { const users = [{ id: 1, name: 'John' }]; jest.spyOn(service, 'findAll').mockResolvedValue(users);
const result = await service.findAll();
expect(result).toEqual(users);
expect(service.findAll).toHaveBeenCalled();
}); });
// Custom provider testing describe('ConfigService', () => { let service: ConfigService;
beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ { provide: ConfigService, useFactory: () => { return new ConfigService('.env.test'); }, }, ], }).compile();
service = module.get<ConfigService>(ConfigService);
});
it('should load config from test environment', () => { expect(service.get('NODE_ENV')).toBe('test'); }); });
Testing Controllers
Mocking services and testing request/response handling.
import { Test, TestingModule } from '@nestjs/testing'; import { UserController } from './user.controller'; import { UserService } from './user.service'; import { CreateUserDto } from './dto/create-user.dto'; import { NotFoundException } from '@nestjs/common';
describe('UserController', () => { let controller: UserController; let service: UserService;
const mockUserService = { findAll: jest.fn(), findOne: jest.fn(), create: jest.fn(), update: jest.fn(), remove: jest.fn(), };
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], providers: [ { provide: UserService, useValue: mockUserService, }, ], }).compile();
controller = module.get<UserController>(UserController);
service = module.get<UserService>(UserService);
});
afterEach(() => { jest.clearAllMocks(); });
describe('findAll', () => { it('should return an array of users', async () => { const users = [ { id: 1, name: 'John', email: 'john@example.com' }, { id: 2, name: 'Jane', email: 'jane@example.com' }, ];
mockUserService.findAll.mockResolvedValue(users);
const result = await controller.findAll();
expect(result).toEqual(users);
expect(service.findAll).toHaveBeenCalledTimes(1);
});
it('should return empty array when no users', async () => {
mockUserService.findAll.mockResolvedValue([]);
const result = await controller.findAll();
expect(result).toEqual([]);
});
});
describe('findOne', () => { it('should return a user by id', async () => { const user = { id: 1, name: 'John', email: 'john@example.com' }; mockUserService.findOne.mockResolvedValue(user);
const result = await controller.findOne('1');
expect(result).toEqual(user);
expect(service.findOne).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException when user not found', async () => {
mockUserService.findOne.mockRejectedValue(
new NotFoundException('User not found'),
);
await expect(controller.findOne('999')).rejects.toThrow(
NotFoundException,
);
});
});
describe('create', () => { it('should create a new user', async () => { const createUserDto: CreateUserDto = { name: 'John', email: 'john@example.com', password: 'password123', };
const createdUser = { id: 1, ...createUserDto };
mockUserService.create.mockResolvedValue(createdUser);
const result = await controller.create(createUserDto);
expect(result).toEqual(createdUser);
expect(service.create).toHaveBeenCalledWith(createUserDto);
});
});
describe('update', () => { it('should update a user', async () => { const updateDto = { name: 'Updated Name' }; const updatedUser = { id: 1, name: 'Updated Name', email: 'john@example.com' };
mockUserService.update.mockResolvedValue(updatedUser);
const result = await controller.update('1', updateDto);
expect(result).toEqual(updatedUser);
expect(service.update).toHaveBeenCalledWith(1, updateDto);
});
});
describe('remove', () => { it('should delete a user', async () => { mockUserService.remove.mockResolvedValue({ deleted: true });
const result = await controller.remove('1');
expect(result).toEqual({ deleted: true });
expect(service.remove).toHaveBeenCalledWith(1);
});
}); });
Testing Services
Mocking repositories and database operations.
import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; import { Repository } from 'typeorm'; import { User } from './entities/user.entity'; import { getRepositoryToken } from '@nestjs/typeorm'; import { NotFoundException, ConflictException } from '@nestjs/common';
describe('UserService', () => { let service: UserService; let repository: Repository<User>;
const mockRepository = { find: jest.fn(), findOne: jest.fn(), findOneBy: jest.fn(), save: jest.fn(), create: jest.fn(), delete: jest.fn(), update: jest.fn(), };
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useValue: mockRepository, }, ], }).compile();
service = module.get<UserService>(UserService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
describe('findAll', () => { it('should return an array of users', async () => { const users = [{ id: 1, name: 'John', email: 'john@example.com' }]; mockRepository.find.mockResolvedValue(users);
const result = await service.findAll();
expect(result).toEqual(users);
expect(repository.find).toHaveBeenCalled();
});
});
describe('findOne', () => { it('should return a user', async () => { const user = { id: 1, name: 'John', email: 'john@example.com' }; mockRepository.findOneBy.mockResolvedValue(user);
const result = await service.findOne(1);
expect(result).toEqual(user);
expect(repository.findOneBy).toHaveBeenCalledWith({ id: 1 });
});
it('should throw NotFoundException when user not found', async () => {
mockRepository.findOneBy.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
});
});
describe('create', () => { it('should create a new user', async () => { const createDto = { name: 'John', email: 'john@example.com', password: 'password123', }; const user = { id: 1, ...createDto };
mockRepository.findOneBy.mockResolvedValue(null); // Email not taken
mockRepository.create.mockReturnValue(user);
mockRepository.save.mockResolvedValue(user);
const result = await service.create(createDto);
expect(result).toEqual(user);
expect(repository.create).toHaveBeenCalledWith(createDto);
expect(repository.save).toHaveBeenCalledWith(user);
});
it('should throw ConflictException when email exists', async () => {
const createDto = {
name: 'John',
email: 'john@example.com',
password: 'password123',
};
mockRepository.findOneBy.mockResolvedValue({ id: 1 }); // Email exists
await expect(service.create(createDto)).rejects.toThrow(
ConflictException,
);
});
});
describe('update', () => { it('should update a user', async () => { const updateDto = { name: 'Updated Name' }; const existingUser = { id: 1, name: 'John', email: 'john@example.com' }; const updatedUser = { ...existingUser, ...updateDto };
mockRepository.findOneBy.mockResolvedValue(existingUser);
mockRepository.save.mockResolvedValue(updatedUser);
const result = await service.update(1, updateDto);
expect(result).toEqual(updatedUser);
expect(repository.save).toHaveBeenCalled();
});
});
describe('remove', () => { it('should delete a user', async () => { const user = { id: 1, name: 'John', email: 'john@example.com' }; mockRepository.findOneBy.mockResolvedValue(user); mockRepository.delete.mockResolvedValue({ affected: 1 });
await service.remove(1);
expect(repository.delete).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException when deleting non-existent user', async () => {
mockRepository.findOneBy.mockResolvedValue(null);
await expect(service.remove(999)).rejects.toThrow(NotFoundException);
});
}); });
Testing Providers
Factory providers and async providers.
import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import { DatabaseService } from './database.service';
describe('Factory Providers', () => { let databaseService: DatabaseService;
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: 'DATABASE_CONNECTION', useFactory: (config: ConfigService) => { return { host: config.get('DB_HOST'), port: config.get('DB_PORT'), database: config.get('DB_NAME'), }; }, inject: [ConfigService], }, { provide: ConfigService, useValue: { get: jest.fn((key: string) => { const config = { DB_HOST: 'localhost', DB_PORT: 5432, DB_NAME: 'test_db', }; return config[key]; }), }, }, DatabaseService, ], }).compile();
databaseService = module.get<DatabaseService>(DatabaseService);
});
it('should create database connection with correct config', () => { const connection = databaseService.getConnection(); expect(connection.host).toBe('localhost'); expect(connection.port).toBe(5432); expect(connection.database).toBe('test_db'); }); });
// Async provider testing describe('Async Providers', () => { let service: any;
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: 'ASYNC_CONNECTION', useFactory: async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return { connected: true }; }, }, ], }).compile();
service = module.get('ASYNC_CONNECTION');
});
it('should resolve async provider', () => { expect(service.connected).toBe(true); }); });
Testing Guards
Authentication and authorization guards.
import { Test, TestingModule } from '@nestjs/testing'; import { JwtAuthGuard } from './jwt-auth.guard'; import { JwtService } from '@nestjs/jwt'; import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
describe('JwtAuthGuard', () => { let guard: JwtAuthGuard; let jwtService: JwtService;
const mockJwtService = { verifyAsync: jest.fn(), };
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ JwtAuthGuard, { provide: JwtService, useValue: mockJwtService, }, ], }).compile();
guard = module.get<JwtAuthGuard>(JwtAuthGuard);
jwtService = module.get<JwtService>(JwtService);
});
it('should allow request with valid token', async () => { const mockContext = createMockExecutionContext({ headers: { authorization: 'Bearer valid-token' }, });
mockJwtService.verifyAsync.mockResolvedValue({
userId: 1,
email: 'user@example.com',
});
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
expect(jwtService.verifyAsync).toHaveBeenCalledWith('valid-token', {
secret: expect.any(String),
});
});
it('should deny request without token', async () => { const mockContext = createMockExecutionContext({ headers: {}, });
await expect(guard.canActivate(mockContext)).rejects.toThrow(
UnauthorizedException,
);
});
it('should deny request with invalid token', async () => { const mockContext = createMockExecutionContext({ headers: { authorization: 'Bearer invalid-token' }, });
mockJwtService.verifyAsync.mockRejectedValue(new Error('Invalid token'));
await expect(guard.canActivate(mockContext)).rejects.toThrow(
UnauthorizedException,
);
}); });
// Helper function function createMockExecutionContext(request: any): ExecutionContext { return { switchToHttp: () => ({ getRequest: () => request, getResponse: () => ({}), }), getHandler: () => ({}), getClass: () => ({}), } as ExecutionContext; }
// Testing RolesGuard import { RolesGuard } from './roles.guard'; import { Reflector } from '@nestjs/core'; import { ForbiddenException } from '@nestjs/common';
describe('RolesGuard', () => { let guard: RolesGuard; let reflector: Reflector;
beforeEach(() => { reflector = new Reflector(); guard = new RolesGuard(reflector); });
it('should allow access when user has required role', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const mockContext = createMockExecutionContext({
user: { id: 1, roles: ['admin'] },
});
const result = guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should deny access when user lacks required role', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const mockContext = createMockExecutionContext({
user: { id: 1, roles: ['user'] },
});
expect(() => guard.canActivate(mockContext)).toThrow(ForbiddenException);
});
it('should allow access when no roles required', () => { jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);
const mockContext = createMockExecutionContext({
user: { id: 1, roles: [] },
});
const result = guard.canActivate(mockContext);
expect(result).toBe(true);
}); });
Testing Interceptors
Transformation and logging interceptors.
import { Test, TestingModule } from '@nestjs/testing'; import { TransformInterceptor } from './transform.interceptor'; import { ExecutionContext, CallHandler } from '@nestjs/common'; import { of } from 'rxjs';
describe('TransformInterceptor', () => { let interceptor: TransformInterceptor;
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [TransformInterceptor], }).compile();
interceptor = module.get<TransformInterceptor>(TransformInterceptor);
});
it('should transform response data', (done) => { const mockContext = { switchToHttp: () => ({ getRequest: () => ({ url: '/test' }), }), } as ExecutionContext;
const mockCallHandler: CallHandler = {
handle: () => of({ name: 'Test', value: 123 }),
};
interceptor.intercept(mockContext, mockCallHandler).subscribe({
next: (result) => {
expect(result).toHaveProperty('data');
expect(result.data).toEqual({ name: 'Test', value: 123 });
expect(result).toHaveProperty('timestamp');
expect(result).toHaveProperty('path');
expect(result.path).toBe('/test');
done();
},
});
}); });
// Testing caching interceptor import { CacheInterceptor } from './cache.interceptor'; import { CACHE_MANAGER } from '@nestjs/cache-manager';
describe('CacheInterceptor', () => { let interceptor: CacheInterceptor; let cacheManager: any;
const mockCacheManager = { get: jest.fn(), set: jest.fn(), };
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ CacheInterceptor, { provide: CACHE_MANAGER, useValue: mockCacheManager, }, ], }).compile();
interceptor = module.get<CacheInterceptor>(CacheInterceptor);
cacheManager = module.get(CACHE_MANAGER);
});
it('should return cached data if available', async (done) => { const cachedData = { cached: true }; mockCacheManager.get.mockResolvedValue(cachedData);
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({ method: 'GET', url: '/test' }),
}),
} as ExecutionContext;
const mockCallHandler: CallHandler = {
handle: () => of({ fresh: true }),
};
const result$ = await interceptor.intercept(mockContext, mockCallHandler);
result$.subscribe({
next: (result) => {
expect(result).toEqual(cachedData);
expect(cacheManager.get).toHaveBeenCalledWith('GET:/test');
done();
},
});
});
it('should cache fresh data', (done) => { const freshData = { fresh: true }; mockCacheManager.get.mockResolvedValue(null);
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({ method: 'GET', url: '/test' }),
}),
} as ExecutionContext;
const mockCallHandler: CallHandler = {
handle: () => of(freshData),
};
interceptor.intercept(mockContext, mockCallHandler).then((result$) => {
result$.subscribe({
next: async (result) => {
expect(result).toEqual(freshData);
// Give time for cache to be set
await new Promise((resolve) => setTimeout(resolve, 100));
expect(cacheManager.set).toHaveBeenCalled();
done();
},
});
});
}); });
Testing Pipes
Validation and transformation pipes.
import { Test, TestingModule } from '@nestjs/testing'; import { ValidationPipe, BadRequestException } from '@nestjs/common'; import { ParseIntPipe } from '@nestjs/common'; import { ArgumentMetadata } from '@nestjs/common';
describe('ParseIntPipe', () => { let pipe: ParseIntPipe;
beforeEach(() => { pipe = new ParseIntPipe(); });
it('should parse valid number string', async () => { const metadata: ArgumentMetadata = { type: 'param', metatype: Number, data: 'id', };
const result = await pipe.transform('123', metadata);
expect(result).toBe(123);
});
it('should throw error for invalid number string', async () => { const metadata: ArgumentMetadata = { type: 'param', metatype: Number, data: 'id', };
await expect(pipe.transform('abc', metadata)).rejects.toThrow(
BadRequestException,
);
}); });
// Custom validation pipe testing import { CustomValidationPipe } from './custom-validation.pipe'; import { IsString, IsEmail, MinLength } from 'class-validator';
class CreateUserDto { @IsString() @MinLength(3) name: string;
@IsEmail() email: string; }
describe('CustomValidationPipe', () => { let pipe: CustomValidationPipe;
beforeEach(() => { pipe = new CustomValidationPipe(); });
it('should validate valid DTO', async () => { const dto = { name: 'John Doe', email: 'john@example.com', };
const metadata: ArgumentMetadata = {
type: 'body',
metatype: CreateUserDto,
};
const result = await pipe.transform(dto, metadata);
expect(result).toEqual(dto);
});
it('should throw error for invalid DTO', async () => { const dto = { name: 'Jo', // Too short email: 'invalid-email', };
const metadata: ArgumentMetadata = {
type: 'body',
metatype: CreateUserDto,
};
await expect(pipe.transform(dto, metadata)).rejects.toThrow(
BadRequestException,
);
}); });
Integration Testing / E2E Tests
Testing with supertest and real HTTP requests.
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from '../src/users/entities/user.entity';
describe('UserController (e2e)', () => { let app: INestApplication; let userRepository: any;
beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
userRepository = moduleFixture.get(getRepositoryToken(User));
await app.init();
});
afterAll(async () => { await app.close(); });
beforeEach(async () => { // Clean database before each test await userRepository.query('DELETE FROM users'); });
describe('/users (POST)', () => { it('should create a new user', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John Doe', email: 'john@example.com', password: 'password123', }) .expect(201) .expect((res) => { expect(res.body).toHaveProperty('id'); expect(res.body.name).toBe('John Doe'); expect(res.body.email).toBe('john@example.com'); expect(res.body).not.toHaveProperty('password'); }); });
it('should return 400 for invalid data', () => {
return request(app.getHttpServer())
.post('/users')
.send({
name: 'Jo', // Too short
email: 'invalid-email',
})
.expect(400);
});
});
describe('/users (GET)', () => { it('should return all users', async () => { // Seed data await userRepository.save([ { name: 'User 1', email: 'user1@example.com' }, { name: 'User 2', email: 'user2@example.com' }, ]);
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(res.body).toHaveLength(2);
expect(res.body[0]).toHaveProperty('id');
expect(res.body[0]).toHaveProperty('name');
});
});
it('should return empty array when no users', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect([]);
});
});
describe('/users/:id (GET)', () => { it('should return a user by id', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', });
return request(app.getHttpServer())
.get(`/users/${user.id}`)
.expect(200)
.expect((res) => {
expect(res.body.id).toBe(user.id);
expect(res.body.name).toBe('John Doe');
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer()).get('/users/999').expect(404);
});
});
describe('/users/:id (PATCH)', () => { it('should update a user', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', });
return request(app.getHttpServer())
.patch(`/users/${user.id}`)
.send({ name: 'Jane Doe' })
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('Jane Doe');
});
});
});
describe('/users/:id (DELETE)', () => { it('should delete a user', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', });
await request(app.getHttpServer())
.delete(`/users/${user.id}`)
.expect(200);
// Verify deletion
const deletedUser = await userRepository.findOne({ where: { id: user.id } });
expect(deletedUser).toBeNull();
});
}); });
Testing with Database
In-memory, Docker, and test containers.
// In-memory SQLite for testing import { Test, TestingModule } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity';
describe('UserService with In-Memory DB', () => { let module: TestingModule; let service: UserService;
beforeAll(async () => { module = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, dropSchema: true, }), TypeOrmModule.forFeature([User]), ], providers: [UserService], }).compile();
service = module.get<UserService>(UserService);
});
afterAll(async () => { await module.close(); });
it('should create and retrieve a user', async () => { const user = await service.create({ name: 'John', email: 'john@example.com', password: 'password123', });
expect(user.id).toBeDefined();
const foundUser = await service.findOne(user.id);
expect(foundUser.name).toBe('John');
}); });
// Test with Docker container (using testcontainers) import { GenericContainer, StartedTestContainer } from 'testcontainers';
describe('UserService with PostgreSQL Container', () => { let container: StartedTestContainer; let module: TestingModule;
beforeAll(async () => { // Start PostgreSQL container container = await new GenericContainer('postgres:15') .withEnvironment({ POSTGRES_USER: 'test', POSTGRES_PASSWORD: 'test', POSTGRES_DB: 'testdb', }) .withExposedPorts(5432) .start();
const port = container.getMappedPort(5432);
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port,
username: 'test',
password: 'test',
database: 'testdb',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UserService],
}).compile();
}, 60000);
afterAll(async () => { await module.close(); await container.stop(); });
it('should work with real PostgreSQL', async () => { const service = module.get<UserService>(UserService); const user = await service.create({ name: 'John', email: 'john@example.com', password: 'password123', });
expect(user.id).toBeDefined();
}); });
Testing WebSockets
WebSocket gateway testing.
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { io, Socket } from 'socket.io-client'; import { ChatGateway } from './chat.gateway';
describe('ChatGateway (e2e)', () => { let app: INestApplication; let clientSocket: Socket;
beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ providers: [ChatGateway], }).compile();
app = moduleFixture.createNestApplication();
await app.listen(3001);
});
afterAll(async () => { await app.close(); });
beforeEach((done) => { clientSocket = io('http://localhost:3001'); clientSocket.on('connect', done); });
afterEach(() => { clientSocket.close(); });
it('should receive messages', (done) => { clientSocket.emit('message', { text: 'Hello World' });
clientSocket.on('message', (data) => {
expect(data.text).toBe('Hello World');
done();
});
});
it('should handle multiple clients', (done) => { const client2 = io('http://localhost:3001');
client2.on('connect', () => {
clientSocket.emit('message', { text: 'Broadcast' });
client2.on('message', (data) => {
expect(data.text).toBe('Broadcast');
client2.close();
done();
});
});
}); });
Testing GraphQL Resolvers
GraphQL testing with supertest.
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
describe('UserResolver (e2e)', () => { let app: INestApplication;
beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: true, }), UserModule, ], }).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => { await app.close(); });
it('should query users', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: query { users { id name email } } ,
})
.expect(200)
.expect((res) => {
expect(res.body.data.users).toBeDefined();
expect(Array.isArray(res.body.data.users)).toBe(true);
});
});
it('should create a user', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: mutation { createUser(createUserInput: { name: "John Doe" email: "john@example.com" }) { id name email } } ,
})
.expect(200)
.expect((res) => {
expect(res.body.data.createUser).toHaveProperty('id');
expect(res.body.data.createUser.name).toBe('John Doe');
});
});
});
Mocking Strategies
jest.mock and custom providers.
// Mock entire module jest.mock('./user.service'); import { UserService } from './user.service';
describe('UserController with mocked service', () => { let controller: UserController;
beforeEach(() => { controller = new UserController(new UserService()); });
it('should use mocked service', async () => { jest.spyOn(UserService.prototype, 'findAll').mockResolvedValue([]); const result = await controller.findAll(); expect(result).toEqual([]); }); });
// Partial mocking const mockUserService = { findAll: jest.fn(), findOne: jest.fn(), } as unknown as UserService;
// Mock external dependencies import axios from 'axios'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('ExternalApiService', () => { it('should fetch data from external API', async () => { mockedAxios.get.mockResolvedValue({ data: { result: 'success' } });
const service = new ExternalApiService();
const result = await service.fetchData();
expect(result).toEqual({ result: 'success' });
expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/data');
}); });
// Custom mock factory function createMockRepository() { return { find: jest.fn(), findOne: jest.fn(), save: jest.fn(), create: jest.fn((dto) => dto), delete: jest.fn(), }; }
Test Fixtures and Factories
Creating reusable test data.
// User factory export class UserFactory { static create(overrides?: Partial<User>): User { return { id: 1, name: 'John Doe', email: 'john@example.com', password: 'hashed_password', createdAt: new Date(), updatedAt: new Date(), ...overrides, }; }
static createMany(count: number, overrides?: Partial<User>): User[] { return Array.from({ length: count }, (_, i) => this.create({ id: i + 1, ...overrides }), ); } }
// Usage in tests describe('UserService', () => { it('should find users', async () => { const users = UserFactory.createMany(3); mockRepository.find.mockResolvedValue(users);
const result = await service.findAll();
expect(result).toHaveLength(3);
}); });
// Builder pattern for complex entities class UserBuilder { private user: Partial<User> = {};
withName(name: string): this { this.user.name = name; return this; }
withEmail(email: string): this { this.user.email = email; return this; }
asAdmin(): this { this.user.role = 'admin'; return this; }
build(): User { return { id: 1, name: 'John Doe', email: 'john@example.com', password: 'password', role: 'user', createdAt: new Date(), updatedAt: new Date(), ...this.user, } as User; } }
// Usage const adminUser = new UserBuilder() .withName('Admin User') .withEmail('admin@example.com') .asAdmin() .build();
Code Coverage and CI/CD
Testing configuration for coverage and automation.
// jest.config.js module.exports = { moduleFileExtensions: ['js', 'json', 'ts'], rootDir: 'src', testRegex: '.\.spec\.ts$', transform: { '^.+\.(t|j)s$': 'ts-jest', }, collectCoverageFrom: [ '**/.(t|j)s', '!/*.module.ts', '!/node_modules/', '!/dist/**', ], coverageDirectory: '../coverage', testEnvironment: 'node', coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }, };
// package.json scripts { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" } }
// GitHub Actions CI // .github/workflows/test.yml name: Tests user-invocable: false
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run test:cov
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
When to Use This Skill
Use nestjs-testing when:
-
Building production applications that require reliability
-
Implementing new features that need verification
-
Refactoring code safely with confidence
-
Debugging complex issues through isolated tests
-
Ensuring API contracts are maintained
-
Validating business logic correctness
-
Setting up CI/CD pipelines
-
Documenting expected behavior through tests
-
Preventing regressions in existing functionality
-
Meeting code quality standards and coverage requirements
NestJS Testing Best Practices
-
Test isolation - Each test should be independent and not rely on others
-
AAA pattern - Structure tests as Arrange, Act, Assert
-
Mock external dependencies - Mock databases, APIs, and third-party services
-
Use factories - Create test data with factories for consistency
-
Test behavior, not implementation - Focus on what the code does, not how
-
Meaningful test names - Describe what is being tested and expected outcome
-
Setup and teardown - Clean up resources after tests
-
Coverage goals - Aim for 80%+ coverage but focus on critical paths
-
E2E for critical flows - Test important user journeys end-to-end
-
Run tests in CI/CD - Automate testing in your deployment pipeline
NestJS Testing Common Pitfalls
-
Testing implementation details - Tests break when refactoring
-
Shared state - Tests fail when run in different orders
-
Not cleaning up - Database pollution between tests
-
Over-mocking - Mocking everything reduces test value
-
Flaky tests - Tests that randomly fail due to timing issues
-
Slow tests - Not using in-memory databases for unit tests
-
Missing edge cases - Only testing happy paths
-
Incomplete mocks - Missing methods on mocked services
-
Not testing errors - Only testing successful scenarios
-
Poor test organization - Hard to find and maintain tests
Resources
-
NestJS Testing Documentation
-
Jest Documentation
-
Supertest Documentation
-
TestContainers Node
-
Testing TypeORM
-
Socket.IO Client Testing
-
Apollo Testing Utilities
-
Code Coverage with Jest