Jest Unit Tests
Overviewβ
Jest is a zero-configuration JavaScript testing framework used in the application for unit testing services, utilities, and components. It provides built-in test runners, mocking capabilities, and coverage reporting.
What is Jest?β
Jest is a popular testing framework that:
- Zero Config: Works out-of-the-box with sensible defaults
- Snapshot Testing: Capture component/output changes
- Mocking: Mock modules, functions, and timers easily
- Coverage Reports: Track code coverage metrics
- Fast: Parallel test execution
- Watch Mode: Automatically run tests on file changes
App Jest Setupβ
Configuration Filesβ
apps/api/jest.config.tsβ
import type { Config } from 'jest';
const config: Config = {
displayName: 'api',
preset: '../../jest.preset.js',
testEnvironment: 'node',
roots: ['<rootDir>'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.module.ts',
'!src/main.ts',
'!src/**/*.interface.ts',
],
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
};
export default config;
apps/web/jest.config.tsβ
import type { Config } from 'jest';
const config: Config = {
displayName: 'web',
preset: '../../jest.preset.js',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
};
export default config;
Root Jest Presetβ
// jest.preset.js
module.exports = {
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
coverageDirectory: '<rootDir>/coverage',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: ['<rootDir>/**/__tests__/**/*.ts?(x)', '<rootDir>/**/?(*.)+(spec|test).ts?(x)'],
};
Running Testsβ
Common Commandsβ
# Run all tests
pnpm test
# Run tests in specific package
pnpm --filter qms-api test
pnpm --filter qms-web test
# Watch mode (rerun on file changes)
pnpm test -- --watch
# Coverage report
pnpm test -- --coverage
# Specific test file
pnpm test -- src/services/user.service.spec.ts
# Tests matching pattern
pnpm test -- --testNamePattern="should create user"
# Update snapshots
pnpm test -- --updateSnapshot
Unit Testing Servicesβ
Example: Service Unit Testβ
// apps/api/src/employees/employees.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { EmployeesService } from './employees.service';
import { PrismaService } from '../prisma/prisma.service';
describe('EmployeesService', () => {
let service: EmployeesService;
let prismaService: PrismaService;
beforeEach(async () => {
// Mock Prisma
const mockPrismaService = {
employee: {
create: jest.fn(),
findMany: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [
EmployeesService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
}).compile();
service = module.get<EmployeesService>(EmployeesService);
prismaService = module.get<PrismaService>(PrismaService);
});
describe('create', () => {
it('should create an employee', async () => {
const createEmployeeDto = {
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
};
const expectedEmployee = {
id: '1',
...createEmployeeDto,
createdAt: new Date(),
};
jest.spyOn(prismaService.employee, 'create').mockResolvedValue(expectedEmployee);
const result = await service.create(createEmployeeDto);
expect(result).toEqual(expectedEmployee);
expect(prismaService.employee.create).toHaveBeenCalledWith({
data: createEmployeeDto,
});
});
it('should throw error for duplicate email', async () => {
const createEmployeeDto = {
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
};
jest
.spyOn(prismaService.employee, 'create')
.mockRejectedValue(new Error('Unique constraint failed'));
await expect(service.create(createEmployeeDto)).rejects.toThrow();
});
});
describe('findAll', () => {
it('should return array of employees', async () => {
const employees = [
{ id: '1', name: 'John', email: 'john@example.com' },
{ id: '2', name: 'Jane', email: 'jane@example.com' },
];
jest.spyOn(prismaService.employee, 'findMany').mockResolvedValue(employees);
const result = await service.findAll();
expect(result).toEqual(employees);
expect(prismaService.employee.findMany).toHaveBeenCalled();
});
it('should return empty array when no employees exist', async () => {
jest.spyOn(prismaService.employee, 'findMany').mockResolvedValue([]);
const result = await service.findAll();
expect(result).toEqual([]);
});
});
describe('findById', () => {
it('should return employee by id', async () => {
const employee = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
};
jest.spyOn(prismaService.employee, 'findUnique').mockResolvedValue(employee);
const result = await service.findById('1');
expect(result).toEqual(employee);
expect(prismaService.employee.findUnique).toHaveBeenCalledWith({
where: { id: '1' },
});
});
it('should return null for non-existent employee', async () => {
jest.spyOn(prismaService.employee, 'findUnique').mockResolvedValue(null);
const result = await service.findById('999');
expect(result).toBeNull();
});
});
});
Unit Testing Components (React)β
Example: Component Unit Testβ
// apps/web/src/components/__tests__/EmployeeCard.spec.tsx
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { EmployeeCard } from '../EmployeeCard';
describe('EmployeeCard', () => {
const mockEmployee = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
};
it('should render employee information', () => {
render(<EmployeeCard employee={mockEmployee} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByText('Engineering')).toBeInTheDocument();
});
it('should call onClick handler when card is clicked', () => {
const handleClick = jest.fn();
render(<EmployeeCard employee={mockEmployee} onClick={handleClick} />);
screen.getByRole('button').click();
expect(handleClick).toHaveBeenCalledWith(mockEmployee.id);
});
it('should display loading state', () => {
render(<EmployeeCard employee={mockEmployee} isLoading={true} />);
expect(screen.getByTestId('skeleton')).toBeInTheDocument();
});
it('should match snapshot', () => {
const { container } = render(<EmployeeCard employee={mockEmployee} />);
expect(container.firstChild).toMatchSnapshot();
});
});
Mocking Strategiesβ
Mock Functionsβ
// Simple mock
const mockFn = jest.fn();
mockFn('arg');
expect(mockFn).toHaveBeenCalledWith('arg');
// Mock return value
const mockFn = jest.fn().mockReturnValue('value');
expect(mockFn()).toBe('value');
// Mock resolved value (for promises)
const mockFn = jest.fn().mockResolvedValue({ id: 1, name: 'John' });
await mockFn(); // Returns { id: 1, name: 'John' }
// Mock rejected value
const mockFn = jest.fn().mockRejectedValue(new Error('Failed'));
await expect(mockFn()).rejects.toThrow('Failed');
Mock Modulesβ
// Mock entire module
jest.mock('../services/api.service', () => ({
ApiService: {
get: jest.fn(),
post: jest.fn(),
},
}));
// Partial mock (keep original implementation)
jest.mock('../services/api.service', () => ({
...jest.requireActual('../services/api.service'),
get: jest.fn(),
}));
// Mock with custom implementation
jest.mock('../utils/logger', () => ({
log: jest.fn((msg) => console.log(`[LOG] ${msg}`)),
}));
Mock Timersβ
describe('Timeout handling', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
it('should execute after delay', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
});
Coverage Reportsβ
Generate Coverageβ
# Generate coverage and open report
pnpm test -- --coverage
open coverage/lcov-report/index.html
# Coverage thresholds
pnpm test -- --coverage --coveragePathIgnorePatterns="/node_modules/"
Coverage Configurationβ
// jest.config.ts
const config: Config = {
// ...
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.interface.ts'],
coverageThreshold: {
global: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
};
Best Practicesβ
1. Test Behavior, Not Implementationβ
// β
GOOD: Test behavior
it('should return sorted employees', () => {
const result = service.getSortedEmployees();
expect(result[0].name).toBe('Alice');
expect(result[1].name).toBe('Bob');
});
// β AVOID: Test implementation details
it('should call sort method', () => {
const sortSpy = jest.spyOn(Array.prototype, 'sort');
service.getSortedEmployees();
expect(sortSpy).toHaveBeenCalled();
});
2. Use Descriptive Test Namesβ
// β
GOOD
it('should return empty array when no employees exist');
it('should throw ConflictException when email is duplicate');
// β AVOID
it('works');
it('test findAll');
3. Follow AAA Pattern (Arrange, Act, Assert)β
it('should create employee with valid data', () => {
// Arrange
const newEmployee = { name: 'John', email: 'john@example.com' };
// Act
const result = service.create(newEmployee);
// Assert
expect(result.id).toBeDefined();
expect(result.name).toBe('John');
});
4. Test Edge Casesβ
describe('parseInt utility', () => {
it('should parse valid number string', () => {
expect(parseInt('123')).toBe(123);
});
it('should handle negative numbers', () => {
expect(parseInt('-123')).toBe(-123);
});
it('should handle invalid input', () => {
expect(parseInt('abc')).toBe(NaN);
});
it('should handle empty string', () => {
expect(parseInt('')).toBe(NaN);
});
});
5. Keep Tests Independentβ
// β
GOOD: Each test is independent
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('test 1', () => {
/* ... */
});
it('test 2', () => {
/* ... */
});
});
// β AVOID: Tests depending on each other
describe('UserService', () => {
const service = new UserService();
let userId: string;
it('should create user', () => {
userId = service.create({}).id; // test 2 depends on this
});
it('should find user', () => {
expect(service.findById(userId)).toBeDefined(); // depends on test 1
});
});
Debugging Testsβ
Run Tests in Debug Modeβ
# Debug in Chrome DevTools
node --inspect-brk node_modules/.bin/jest --runInBand
# Then open chrome://inspect
Print Debug Informationβ
it('should debug something', () => {
console.log('Debug info:', data);
console.table(employees);
debugger; // Pauses execution
});
Common Assertionsβ
// Equality
expect(value).toBe(5);
expect(obj).toEqual({ id: 1 });
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3);
// Strings
expect(str).toMatch(/pattern/);
expect(str).toContain('substring');
// Arrays
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// Functions
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith('arg');
expect(fn).toHaveBeenCalledTimes(1);
// Promises
expect(promise).resolves.toEqual(value);
expect(promise).rejects.toThrow();
Next Stepsβ
- Learn about Supertest for API integration testing
- Set up Bruno for API collection testing
- Explore E2E testing with Robot Framework