Unit_testing
Chapter 41: Unit Testing
Section titled “Chapter 41: Unit Testing”Testing TypeORM Components in Isolation
Section titled “Testing TypeORM Components in Isolation”41.1 Unit Testing Overview
Section titled “41.1 Unit Testing Overview”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
================================================================================41.2 Testing Setup
Section titled “41.2 Testing Setup”Jest Configuration
Section titled “Jest Configuration”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.tsimport { Logger } from '@nestjs/common';
// Suppress logs during testsLogger.overrideLogger(['error', 'warn']);Test Utilities
Section titled “Test Utilities”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.tsexport 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,});41.3 Testing Services
Section titled “41.3 Testing Services”Service Unit Test
Section titled “Service Unit Test”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); }); });});41.4 Testing Controllers
Section titled “41.4 Testing Controllers”Controller Unit Test
Section titled “Controller Unit Test”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); }); });});41.5 Testing Custom Repositories
Section titled “41.5 Testing Custom Repositories”Custom Repository Test
Section titled “Custom Repository Test”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'); }); });});41.6 Testing Entities
Section titled “41.6 Testing Entities”Entity Test
Section titled “Entity Test”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'); }); });});41.7 Testing Guards and Interceptors
Section titled “41.7 Testing Guards and Interceptors”Guard Test
Section titled “Guard Test”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); });});Interceptor Test
Section titled “Interceptor Test”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, }); });});41.8 Test Coverage
Section titled “41.8 Test Coverage”Coverage Configuration
Section titled “Coverage Configuration”{ "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
Section titled “Coverage Report” 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 | ------------------------|---------|----------|---------|---------|
================================================================================41.9 Summary
Section titled “41.9 Summary” 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 | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 42: Integration Testing
Last Updated: February 2026