Skip to main content

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
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​

Resources​