Skip to content

Unit_testing


Unit tests verify individual components in isolation using mocks and stubs.

Unit Testing Pyramid
================================================================================
+--------+
/ E2E \ Few, Slow, Expensive
/ Tests \
+------------+
/ Integration \
/ Tests \ Some, Medium Speed
+------------------+
/ Unit Tests \
/ \ Many, Fast, Cheap
+------------------------+
Unit Testing Principles:
- Test one thing at a time
- Use mocks for dependencies
- Fast execution
- Independent tests
================================================================================

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',
'!**/main.ts',
],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/$1',
},
setupFilesAfterEnv: ['./test/setup.ts'],
};
// test/setup.ts
import { Logger } from '@nestjs/common';
// Suppress logs during tests
Logger.overrideLogger(['error', 'warn']);
test/utils/test-utils.ts
import { Repository } from 'typeorm';
export class MockRepository<T> extends Repository<T> {
private items: T[] = [];
setItems(items: T[]) {
this.items = items;
}
find(options?: any): Promise<T[]> {
return Promise.resolve(this.items);
}
findOne(options: any): Promise<T | null> {
const item = this.items.find(i => (i as any).id === options.where.id);
return Promise.resolve(item || null);
}
save(entity: any): Promise<T> {
if (entity.id) {
const index = this.items.findIndex(i => (i as any).id === entity.id);
if (index >= 0) {
this.items[index] = { ...this.items[index], ...entity };
return Promise.resolve(this.items[index]);
}
}
const newEntity = { ...entity, id: this.items.length + 1 };
this.items.push(newEntity);
return Promise.resolve(newEntity);
}
delete(options: any): Promise<void> {
const id = options.where?.id || options;
this.items = this.items.filter(i => (i as any).id !== id);
return Promise.resolve();
}
create(entity: any): T {
return entity as T;
}
}
// test/utils/mock-utils.ts
export const createMock = <T>(overrides: Partial<T> = {}): T => {
return { ...overrides } as T;
};
export const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
findOneOrFail: jest.fn(),
save: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
remove: jest.fn(),
create: jest.fn(),
merge: jest.fn(),
count: jest.fn(),
increment: jest.fn(),
decrement: jest.fn(),
findAndCount: jest.fn(),
findByIds: jest.fn(),
clear: jest.fn(),
query: jest.fn(),
metadata: {} as any,
target: {} as any,
manager: {} as any,
queryRunner: {} as any,
});

src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { NotFoundException, ConflictException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
let repository: jest.Mocked<Repository<User>>;
const mockUser: User = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
password: 'hashed_password',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get(getRepositoryToken(User));
});
describe('findAll', () => {
it('should return an array of users', async () => {
repository.find.mockResolvedValue([mockUser]);
const result = await service.findAll();
expect(result).toEqual([mockUser]);
expect(repository.find).toHaveBeenCalled();
});
it('should return empty array when no users', async () => {
repository.find.mockResolvedValue([]);
const result = await service.findAll();
expect(result).toEqual([]);
});
});
describe('findOne', () => {
it('should return a user by id', async () => {
repository.findOne.mockResolvedValue(mockUser);
const result = await service.findOne(1);
expect(result).toEqual(mockUser);
expect(repository.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
it('should throw NotFoundException when user not found', async () => {
repository.findOne.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create a new user', async () => {
const createDto = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
};
repository.findOne.mockResolvedValue(null);
repository.create.mockReturnValue({ ...createDto, id: 1 } as User);
repository.save.mockResolvedValue({ ...createDto, id: 1 } as User);
const result = await service.create(createDto);
expect(result).toEqual({ ...createDto, id: 1 });
expect(repository.create).toHaveBeenCalledWith(createDto);
expect(repository.save).toHaveBeenCalled();
});
it('should throw ConflictException when email exists', async () => {
const createDto = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
};
repository.findOne.mockResolvedValue(mockUser);
await expect(service.create(createDto)).rejects.toThrow(ConflictException);
});
});
describe('update', () => {
it('should update an existing user', async () => {
const updateDto = { name: 'Jane Doe' };
const updatedUser = { ...mockUser, name: 'Jane Doe' };
repository.findOne.mockResolvedValue(mockUser);
repository.save.mockResolvedValue(updatedUser);
const result = await service.update(1, updateDto);
expect(result.name).toBe('Jane Doe');
expect(repository.save).toHaveBeenCalled();
});
it('should throw NotFoundException when user not found', async () => {
repository.findOne.mockResolvedValue(null);
await expect(service.update(999, { name: 'Test' })).rejects.toThrow(
NotFoundException,
);
});
});
describe('remove', () => {
it('should delete a user', async () => {
repository.findOne.mockResolvedValue(mockUser);
repository.delete.mockResolvedValue({ affected: 1, raw: [] });
await service.remove(1);
expect(repository.delete).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException when user not found', async () => {
repository.findOne.mockResolvedValue(null);
await expect(service.remove(999)).rejects.toThrow(NotFoundException);
});
});
});

src/users/users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { NotFoundException } from '@nestjs/common';
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
const mockUser: User = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
password: 'hashed_password',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const mockService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get(UsersService);
});
describe('findAll', () => {
it('should return an array of users', async () => {
service.findAll.mockResolvedValue([mockUser]);
const result = await controller.findAll();
expect(result).toEqual([mockUser]);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return a user', async () => {
service.findOne.mockResolvedValue(mockUser);
const result = await controller.findOne(1);
expect(result).toEqual(mockUser);
expect(service.findOne).toHaveBeenCalledWith(1);
});
it('should throw NotFoundException', async () => {
service.findOne.mockRejectedValue(new NotFoundException());
await expect(controller.findOne(999)).rejects.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create a user', async () => {
const createDto = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
};
service.create.mockResolvedValue(mockUser);
const result = await controller.create(createDto);
expect(result).toEqual(mockUser);
expect(service.create).toHaveBeenCalledWith(createDto);
});
});
describe('update', () => {
it('should update a user', async () => {
const updateDto = { name: 'Jane Doe' };
const updatedUser = { ...mockUser, name: 'Jane Doe' };
service.update.mockResolvedValue(updatedUser);
const result = await controller.update(1, updateDto);
expect(result.name).toBe('Jane Doe');
expect(service.update).toHaveBeenCalledWith(1, updateDto);
});
});
describe('remove', () => {
it('should remove a user', async () => {
service.remove.mockResolvedValue(undefined);
await controller.remove(1);
expect(service.remove).toHaveBeenCalledWith(1);
});
});
});

src/users/user.repository.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource, SelectQueryBuilder } from 'typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
describe('UserRepository', () => {
let repository: UserRepository;
let mockQueryBuilder: jest.Mocked<SelectQueryBuilder<User>>;
beforeEach(async () => {
mockQueryBuilder = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
leftJoin: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getOne: jest.fn(),
getMany: jest.fn(),
getRawOne: jest.fn(),
getRawMany: jest.fn(),
getManyAndCount: jest.fn(),
setParameter: jest.fn().mockReturnThis(),
setParameters: jest.fn().mockReturnThis(),
} as any;
const mockRepository = {
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
findOne: jest.fn(),
find: jest.fn(),
save: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserRepository,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
repository = module.get<UserRepository>(UserRepository);
});
describe('findByEmail', () => {
it('should find user by email', async () => {
const mockUser = { id: 1, email: 'test@example.com' } as User;
mockQueryBuilder.getOne.mockResolvedValue(mockUser);
const result = await repository.findByEmail('test@example.com');
expect(result).toEqual(mockUser);
expect(mockQueryBuilder.where).toHaveBeenCalledWith(
'user.email = :email',
{ email: 'test@example.com' }
);
});
it('should return null when user not found', async () => {
mockQueryBuilder.getOne.mockResolvedValue(null);
const result = await repository.findByEmail('nonexistent@example.com');
expect(result).toBeNull();
});
});
describe('findActive', () => {
it('should return active users', async () => {
const mockUsers = [
{ id: 1, isActive: true },
{ id: 2, isActive: true },
] as User[];
mockQueryBuilder.getMany.mockResolvedValue(mockUsers);
const result = await repository.findActive();
expect(result).toEqual(mockUsers);
expect(mockQueryBuilder.where).toHaveBeenCalledWith('user.isActive = :isActive', {
isActive: true,
});
});
});
describe('findWithPostCount', () => {
it('should return users with post count', async () => {
const mockResult = [
{ userId: 1, userName: 'John', postCount: '5' },
{ userId: 2, userName: 'Jane', postCount: '3' },
];
mockQueryBuilder.getRawMany.mockResolvedValue(mockResult);
const result = await repository.findWithPostCount();
expect(result).toEqual(mockResult);
expect(mockQueryBuilder.leftJoin).toHaveBeenCalled();
expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith('COUNT(post.id)', 'postCount');
});
});
});

src/users/user.entity.spec.ts
import { User } from './user.entity';
describe('User Entity', () => {
describe('constructor', () => {
it('should create a user with all properties', () => {
const user = new User();
user.id = 1;
user.name = 'John Doe';
user.email = 'john@example.com';
user.isActive = true;
expect(user.id).toBe(1);
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
expect(user.isActive).toBe(true);
});
});
describe('methods', () => {
it('should return initials from name', () => {
const user = new User();
user.name = 'John Doe';
// Assuming you have a method like this
expect(user.initials).toBe('JD');
});
it('should handle single name', () => {
const user = new User();
user.name = 'John';
expect(user.initials).toBe('J');
});
});
});

src/auth/guards/jwt-auth.guard.spec.ts
import { JwtAuthGuard } from './jwt-auth.guard';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
beforeEach(() => {
guard = new JwtAuthGuard();
});
it('should allow access with valid token', () => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({
user: { id: 1, email: 'test@example.com' },
}),
}),
} as ExecutionContext;
const result = guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should throw UnauthorizedException without user', () => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({}),
}),
} as ExecutionContext;
expect(() => guard.canActivate(mockContext)).toThrow(UnauthorizedException);
});
});
src/common/interceptors/logging.interceptor.spec.ts
import { LoggingInterceptor } from './logging.interceptor';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';
describe('LoggingInterceptor', () => {
let interceptor: LoggingInterceptor;
beforeEach(() => {
interceptor = new LoggingInterceptor();
});
it('should log request and response', (done) => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({
method: 'GET',
url: '/users',
}),
getResponse: () => ({
statusCode: 200,
}),
}),
getHandler: () => jest.fn(),
} as any;
const mockNext: CallHandler = {
handle: () => of({ data: 'test' }),
};
interceptor.intercept(mockContext, mockNext).subscribe({
next: (result) => {
expect(result).toEqual({ data: 'test' });
done();
},
error: done.fail,
});
});
});

package.json
{
"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"
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Coverage Report Example
================================================================================
File | % Stmts | % Branch | % Funcs | % Lines |
------------------------|---------|----------|---------|---------|
All files | 85.23 | 78.45 | 90.12 | 84.56 |
src/users | 92.34 | 85.67 | 95.43 | 91.23 |
users.service.ts | 95.12 | 88.34 | 98.12 | 94.56 |
users.controller.ts | 89.45 | 82.12 | 92.34 | 88.12 |
src/posts | 78.45 | 71.23 | 85.67 | 77.89 |
posts.service.ts | 82.34 | 75.45 | 88.12 | 81.23 |
------------------------|---------|----------|---------|---------|
================================================================================

Unit Testing Quick Reference
+------------------------------------------------------------------+
| |
| Test Type | Purpose |
| -------------------|------------------------------------------|
| Service tests | Test business logic |
| Controller tests | Test HTTP handling |
| Repository tests | Test data access |
| Entity tests | Test entity behavior |
| Guard tests | Test authorization |
| Interceptor tests| Test request/response modification |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Mock dependencies| Isolate unit under test |
| One assertion | Each test verifies one thing |
| Descriptive names| Test names describe behavior |
| Arrange-Act-Assert| Structure tests clearly |
| Coverage goals | Aim for 80%+ coverage |
| |
+------------------------------------------------------------------+

Chapter 42: Integration Testing


Last Updated: February 2026