Prerequisites
- Basic TypeScript syntax and types π
- Understanding of JavaScript testing concepts π§ͺ
- Node.js and npm/pnpm basics π¦
What you'll learn
- Configure Jest for TypeScript with complete type safety π‘οΈ
- Write type-safe unit tests with intelligent autocomplete β¨
- Set up advanced Jest features like mocking and coverage π
- Create a professional testing workflow and automation π
π― Introduction
Welcome to the world of professional TypeScript testing with Jest! π In this guide, weβll transform you from a testing novice into a Jest master who writes bulletproof tests with complete type safety.
Youβll discover how to configure Jest to work seamlessly with TypeScript, enabling you to catch bugs before they reach production π. Whether youβre building React applications π, Node.js APIs π₯οΈ, or utility libraries π, mastering Jest with TypeScript is essential for maintaining high-quality, reliable codebases.
By the end of this tutorial, youβll be writing tests that are not just functional, but elegantly typed and impossible to break! π Letβs dive in! πββοΈ
π Understanding Jest with TypeScript
π€ What is Jest?
Jest is like a Swiss Army knife for JavaScript testing π§. Think of it as your personal quality assurance team that works 24/7 to ensure your code behaves exactly as expected. Itβs the most popular testing framework for JavaScript and TypeScript projects.
In TypeScript terms, Jest provides:
- β¨ Zero-config setup - works out of the box with sensible defaults
- π Built-in assertions - comprehensive expect() API for all test scenarios
- π‘οΈ Mocking capabilities - isolate units under test from dependencies
- π Code coverage - measure how much of your code is tested
π‘ Why Use Jest with TypeScript?
Hereβs why this combination is powerful:
- Type Safety in Tests π: Catch test errors at compile time
- IntelliSense Support π§ : Get autocomplete for your test APIs
- Refactoring Confidence π§: Change code without breaking tests
- Professional Workflows π: Industry-standard testing practices
- Team Collaboration π₯: Self-documenting test specifications
Real-world example: When testing a user authentication system π, Jest with TypeScript ensures your test data matches your User interface, preventing runtime surprises!
π§ Jest Installation and Setup
π¦ Installing Jest Dependencies
Letβs set up Jest for TypeScript step by step:
# π¦ Install Jest and TypeScript support
pnpm add -D jest @types/jest ts-jest
# π οΈ For React projects, also add:
pnpm add -D @testing-library/react @testing-library/jest-dom
# π― For Node.js projects, consider:
pnpm add -D supertest @types/supertest
βοΈ Jest Configuration (jest.config.js)
// π jest.config.js - Complete Jest configuration
const config = {
// π― Use ts-jest preset for TypeScript support
preset: 'ts-jest',
// π Test environment - 'node' for backend, 'jsdom' for frontend
testEnvironment: 'node', // or 'jsdom' for React apps
// π Root directory for tests and modules
rootDir: 'src',
// π Test file patterns
testMatch: [
'**/__tests__/**/*.test.ts',
'**/__tests__/**/*.spec.ts',
'**/*.test.ts',
'**/*.spec.ts'
],
// π Collect coverage from these files
collectCoverageFrom: [
'**/*.{ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
'!**/__tests__/**'
],
// π¨ Coverage reporting
coverageReporters: ['text', 'lcov', 'html'],
// π« Coverage thresholds (fail if below)
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// π οΈ Setup files run before each test
setupFilesAfterEnv: ['<rootDir>/__tests__/setup.ts'],
// ποΈ Module path mapping (match your tsconfig.json)
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/$1',
'^@components/(.*)$': '<rootDir>/components/$1',
'^@utils/(.*)$': '<rootDir>/utils/$1'
},
// π Transform configuration
transform: {
'^.+\\.tsx?$': ['ts-jest', {
// π Speed up compilation
isolatedModules: true,
// π Use project's TypeScript config
tsconfig: 'tsconfig.json'
}]
},
// π File extensions to consider
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
// π Optimize test performance
maxWorkers: '50%',
// π Clear mocks between tests
clearMocks: true,
// π Verbose output for debugging
verbose: true
};
module.exports = config;
π TypeScript Configuration Updates
// π tsconfig.json - Update for Jest compatibility
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"lib": ["es2020", "dom"],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
// π― Path mapping for cleaner imports
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
},
// π§ͺ Include Jest types
"types": ["jest", "@testing-library/jest-dom"]
},
"include": [
"src/**/*",
"**/*.test.ts",
"**/*.spec.ts"
],
"exclude": [
"node_modules",
"dist",
"coverage"
]
}
π§ͺ Writing Your First TypeScript Tests
π Basic Test Structure
// π __tests__/setup.ts - Global test setup
import '@testing-library/jest-dom';
// π― Global test configuration
beforeAll(() => {
console.log('π Starting test suite...');
});
afterAll(() => {
console.log('β
Test suite completed!');
});
// π§Ή Clean up after each test
afterEach(() => {
jest.clearAllMocks();
});
// π utils/math.ts - Code to test
export interface CalculationResult {
value: number;
operation: string;
timestamp: Date;
}
export class Calculator {
private history: CalculationResult[] = [];
// β Addition with history tracking
add(a: number, b: number): CalculationResult {
const result: CalculationResult = {
value: a + b,
operation: `${a} + ${b}`,
timestamp: new Date()
};
this.history.push(result);
return result;
}
// βοΈ Multiplication with validation
multiply(a: number, b: number): CalculationResult {
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new Error('Invalid input: numbers must be finite π«');
}
const result: CalculationResult = {
value: a * b,
operation: `${a} Γ ${b}`,
timestamp: new Date()
};
this.history.push(result);
return result;
}
// π Get calculation history
getHistory(): CalculationResult[] {
return [...this.history]; // π Return copy to prevent mutation
}
// π§Ή Clear calculation history
clearHistory(): void {
this.history = [];
}
}
// π― Utility function for formatting
export const formatResult = (result: CalculationResult): string => {
return `${result.operation} = ${result.value}`;
};
// β° Async operation simulation
export const calculateAsync = async (
operation: (a: number, b: number) => number,
a: number,
b: number,
delay: number = 100
): Promise<number> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(operation(a, b));
}, delay);
});
};
β Comprehensive Test Suite
// π __tests__/math.test.ts - Complete test suite
import { Calculator, formatResult, calculateAsync, type CalculationResult } from '../utils/math';
// π― Group related tests
describe('Calculator Class', () => {
let calculator: Calculator;
// π Fresh instance for each test
beforeEach(() => {
calculator = new Calculator();
});
// β Addition tests
describe('add method', () => {
it('should add two positive numbers correctly', () => {
// π― Arrange
const a = 5;
const b = 3;
// π¬ Act
const result = calculator.add(a, b);
// β
Assert
expect(result.value).toBe(8);
expect(result.operation).toBe('5 + 3');
expect(result.timestamp).toBeInstanceOf(Date);
});
it('should handle negative numbers', () => {
const result = calculator.add(-5, 3);
expect(result.value).toBe(-2);
expect(result.operation).toBe('-5 + 3');
});
it('should handle decimal numbers', () => {
const result = calculator.add(0.1, 0.2);
// π― Use toBeCloseTo for floating point comparisons
expect(result.value).toBeCloseTo(0.3);
});
});
// βοΈ Multiplication tests
describe('multiply method', () => {
it('should multiply two numbers correctly', () => {
const result = calculator.multiply(4, 5);
expect(result.value).toBe(20);
expect(result.operation).toBe('4 Γ 5');
});
it('should throw error for invalid inputs', () => {
// π« Test error cases
expect(() => calculator.multiply(Infinity, 5)).toThrow('Invalid input: numbers must be finite π«');
expect(() => calculator.multiply(5, NaN)).toThrow('Invalid input: numbers must be finite π«');
});
it('should handle zero multiplication', () => {
const result = calculator.multiply(5, 0);
expect(result.value).toBe(0);
});
});
// π History tracking tests
describe('history functionality', () => {
it('should track calculation history', () => {
calculator.add(1, 2);
calculator.multiply(3, 4);
const history = calculator.getHistory();
expect(history).toHaveLength(2);
expect(history[0].operation).toBe('1 + 2');
expect(history[1].operation).toBe('3 Γ 4');
});
it('should return copy of history (immutability)', () => {
calculator.add(1, 2);
const history1 = calculator.getHistory();
const history2 = calculator.getHistory();
// π Should be different arrays with same content
expect(history1).not.toBe(history2);
expect(history1).toEqual(history2);
});
it('should clear history correctly', () => {
calculator.add(1, 2);
calculator.multiply(3, 4);
expect(calculator.getHistory()).toHaveLength(2);
calculator.clearHistory();
expect(calculator.getHistory()).toHaveLength(0);
});
});
});
// π¨ Utility function tests
describe('formatResult function', () => {
it('should format result correctly', () => {
const mockResult: CalculationResult = {
value: 10,
operation: '5 + 5',
timestamp: new Date()
};
const formatted = formatResult(mockResult);
expect(formatted).toBe('5 + 5 = 10');
});
});
// β° Async function tests
describe('calculateAsync function', () => {
it('should perform async calculation', async () => {
const addOperation = (a: number, b: number) => a + b;
// π― Test async operation
const result = await calculateAsync(addOperation, 5, 3, 50);
expect(result).toBe(8);
});
it('should handle custom delay', async () => {
const multiplyOperation = (a: number, b: number) => a * b;
const startTime = Date.now();
await calculateAsync(multiplyOperation, 2, 3, 100);
const endTime = Date.now();
// β±οΈ Should take at least 100ms
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
});
it('should work with different operations', async () => {
const divideOperation = (a: number, b: number) => a / b;
const result = await calculateAsync(divideOperation, 10, 2);
expect(result).toBe(5);
});
});
π Advanced Jest Features
π― Mocking with Type Safety
// π services/userService.ts - Service to test
export interface User {
id: string;
name: string;
email: string;
isActive: boolean;
}
export interface UserRepository {
findById(id: string): Promise<User | null>;
create(user: Omit<User, 'id'>): Promise<User>;
update(id: string, updates: Partial<User>): Promise<User>;
}
export class UserService {
constructor(private userRepo: UserRepository) {}
// π€ Get active user by ID
async getActiveUser(id: string): Promise<User | null> {
const user = await this.userRepo.findById(id);
return user?.isActive ? user : null;
}
// β¨ Create new user with validation
async createUser(userData: Omit<User, 'id'>): Promise<User> {
if (!userData.email.includes('@')) {
throw new Error('Invalid email format π§');
}
return this.userRepo.create({
...userData,
isActive: true // π― New users are active by default
});
}
}
// π __tests__/userService.test.ts - Mocking tests
import { UserService, type User, type UserRepository } from '../services/userService';
// π Create typed mock
const mockUserRepo: jest.Mocked<UserRepository> = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn()
};
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService(mockUserRepo);
// π§Ή Clear mocks between tests
jest.clearAllMocks();
});
describe('getActiveUser', () => {
it('should return active user', async () => {
// π― Arrange - setup mock data
const mockUser: User = {
id: '123',
name: 'John Doe',
email: '[email protected]',
isActive: true
};
mockUserRepo.findById.mockResolvedValue(mockUser);
// π¬ Act
const result = await userService.getActiveUser('123');
// β
Assert
expect(result).toEqual(mockUser);
expect(mockUserRepo.findById).toHaveBeenCalledWith('123');
});
it('should return null for inactive user', async () => {
const inactiveUser: User = {
id: '123',
name: 'John Doe',
email: '[email protected]',
isActive: false
};
mockUserRepo.findById.mockResolvedValue(inactiveUser);
const result = await userService.getActiveUser('123');
expect(result).toBeNull();
});
it('should return null when user not found', async () => {
mockUserRepo.findById.mockResolvedValue(null);
const result = await userService.getActiveUser('999');
expect(result).toBeNull();
});
});
describe('createUser', () => {
it('should create user successfully', async () => {
const userData = {
name: 'Jane Smith',
email: '[email protected]',
isActive: false
};
const createdUser: User = {
id: '456',
...userData,
isActive: true // π― Service sets to true
};
mockUserRepo.create.mockResolvedValue(createdUser);
const result = await userService.createUser(userData);
expect(result).toEqual(createdUser);
expect(mockUserRepo.create).toHaveBeenCalledWith({
...userData,
isActive: true
});
});
it('should throw error for invalid email', async () => {
const invalidUserData = {
name: 'Invalid User',
email: 'invalid-email', // π« Missing @
isActive: true
};
await expect(userService.createUser(invalidUserData))
.rejects
.toThrow('Invalid email format π§');
// π― Should not call repository
expect(mockUserRepo.create).not.toHaveBeenCalled();
});
});
});
π Test Coverage and Reporting
π― Package.json Scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --coverage --watchAll=false",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand"
}
}
π Coverage Configuration
// π jest.config.js - Advanced coverage setup
module.exports = {
// ... other config
// π Detailed coverage collection
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/__tests__/**',
'!src/**/__mocks__/**',
'!src/index.ts'
],
// π― Coverage thresholds per path
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
},
'./src/utils/': {
branches: 85,
functions: 85,
lines: 85,
statements: 85
}
},
// π Multiple coverage reporters
coverageReporters: [
'text', // πΊ Console output
'text-summary', // π Brief summary
'lcov', // π For external tools
'html', // π HTML report
'json' // π JSON data
],
// π Coverage output directory
coverageDirectory: 'coverage'
};
π Testing Best Practices
β Doβs and Donβts
// β
GOOD: Descriptive test names
describe('Calculator add method', () => {
it('should return correct sum for positive integers', () => {
// Test implementation
});
it('should handle negative numbers correctly', () => {
// Test implementation
});
});
// β BAD: Vague test names
describe('Calculator', () => {
it('works', () => {
// What does "works" mean?
});
});
// β
GOOD: Arrange-Act-Assert pattern
it('should calculate compound interest correctly', () => {
// π― Arrange
const principal = 1000;
const rate = 0.05;
const time = 2;
// π¬ Act
const result = calculateCompoundInterest(principal, rate, time);
// β
Assert
expect(result).toBeCloseTo(1102.50, 2);
});
// β
GOOD: Type-safe test data
interface TestUser {
id: string;
name: string;
email: string;
}
const createTestUser = (overrides: Partial<TestUser> = {}): TestUser => {
return {
id: '123',
name: 'Test User',
email: '[email protected]',
...overrides
};
};
// β
GOOD: Testing error conditions
it('should throw error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
// β
GOOD: Async testing
it('should fetch user data successfully', async () => {
const userData = await fetchUser('123');
expect(userData).toMatchObject({
id: '123',
name: expect.any(String),
email: expect.stringContaining('@')
});
});
π― Practical Exercise: E-Commerce Testing
// π models/product.ts - Product management system
export interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
isActive: boolean;
}
export interface CartItem {
productId: string;
quantity: number;
priceAtTime: number;
}
export class ShoppingCart {
private items: Map<string, CartItem> = new Map();
// π Add item to cart
addItem(product: Product, quantity: number): void {
if (!product.isActive) {
throw new Error(`Product ${product.name} is not available π«`);
}
if (quantity > product.stock) {
throw new Error(`Not enough stock. Available: ${product.stock} π¦`);
}
const existingItem = this.items.get(product.id);
if (existingItem) {
const newQuantity = existingItem.quantity + quantity;
if (newQuantity > product.stock) {
throw new Error(`Total quantity exceeds stock π¦`);
}
this.items.set(product.id, {
...existingItem,
quantity: newQuantity
});
} else {
this.items.set(product.id, {
productId: product.id,
quantity,
priceAtTime: product.price
});
}
}
// ποΈ Remove item from cart
removeItem(productId: string): boolean {
return this.items.delete(productId);
}
// π Update item quantity
updateQuantity(productId: string, quantity: number): void {
if (quantity <= 0) {
this.removeItem(productId);
return;
}
const item = this.items.get(productId);
if (item) {
this.items.set(productId, { ...item, quantity });
}
}
// π° Calculate total
getTotal(): number {
let total = 0;
for (const item of this.items.values()) {
total += item.priceAtTime * item.quantity;
}
return Math.round(total * 100) / 100; // π― Round to 2 decimals
}
// π Get all items
getItems(): CartItem[] {
return Array.from(this.items.values());
}
// π§Ή Clear cart
clear(): void {
this.items.clear();
}
// π Get item count
getItemCount(): number {
let count = 0;
for (const item of this.items.values()) {
count += item.quantity;
}
return count;
}
}
// π __tests__/shoppingCart.test.ts - Complete test suite
import { ShoppingCart, type Product, type CartItem } from '../models/product';
describe('ShoppingCart', () => {
let cart: ShoppingCart;
let mockProduct: Product;
beforeEach(() => {
cart = new ShoppingCart();
mockProduct = {
id: 'laptop-001',
name: 'Gaming Laptop',
price: 1299.99,
stock: 5,
category: 'Electronics',
isActive: true
};
});
describe('addItem', () => {
it('should add new item to cart', () => {
cart.addItem(mockProduct, 2);
const items = cart.getItems();
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({
productId: 'laptop-001',
quantity: 2,
priceAtTime: 1299.99
});
});
it('should increase quantity for existing item', () => {
cart.addItem(mockProduct, 1);
cart.addItem(mockProduct, 2);
const items = cart.getItems();
expect(items).toHaveLength(1);
expect(items[0].quantity).toBe(3);
});
it('should throw error for inactive product', () => {
const inactiveProduct = { ...mockProduct, isActive: false };
expect(() => cart.addItem(inactiveProduct, 1))
.toThrow('Product Gaming Laptop is not available π«');
});
it('should throw error when quantity exceeds stock', () => {
expect(() => cart.addItem(mockProduct, 10))
.toThrow('Not enough stock. Available: 5 π¦');
});
it('should throw error when total quantity exceeds stock', () => {
cart.addItem(mockProduct, 3);
expect(() => cart.addItem(mockProduct, 3))
.toThrow('Total quantity exceeds stock π¦');
});
});
describe('removeItem', () => {
it('should remove item from cart', () => {
cart.addItem(mockProduct, 2);
const removed = cart.removeItem('laptop-001');
expect(removed).toBe(true);
expect(cart.getItems()).toHaveLength(0);
});
it('should return false for non-existent item', () => {
const removed = cart.removeItem('non-existent');
expect(removed).toBe(false);
});
});
describe('updateQuantity', () => {
beforeEach(() => {
cart.addItem(mockProduct, 2);
});
it('should update item quantity', () => {
cart.updateQuantity('laptop-001', 4);
const items = cart.getItems();
expect(items[0].quantity).toBe(4);
});
it('should remove item when quantity is zero', () => {
cart.updateQuantity('laptop-001', 0);
expect(cart.getItems()).toHaveLength(0);
});
it('should remove item when quantity is negative', () => {
cart.updateQuantity('laptop-001', -1);
expect(cart.getItems()).toHaveLength(0);
});
});
describe('getTotal', () => {
it('should calculate correct total for single item', () => {
cart.addItem(mockProduct, 2);
expect(cart.getTotal()).toBe(2599.98);
});
it('should calculate correct total for multiple items', () => {
const mouseProduct: Product = {
id: 'mouse-001',
name: 'Gaming Mouse',
price: 79.99,
stock: 10,
category: 'Electronics',
isActive: true
};
cart.addItem(mockProduct, 1);
cart.addItem(mouseProduct, 2);
expect(cart.getTotal()).toBe(1459.97);
});
it('should return 0 for empty cart', () => {
expect(cart.getTotal()).toBe(0);
});
});
describe('getItemCount', () => {
it('should return correct item count', () => {
cart.addItem(mockProduct, 2);
expect(cart.getItemCount()).toBe(2);
});
it('should return 0 for empty cart', () => {
expect(cart.getItemCount()).toBe(0);
});
});
describe('clear', () => {
it('should clear all items from cart', () => {
cart.addItem(mockProduct, 2);
cart.clear();
expect(cart.getItems()).toHaveLength(0);
expect(cart.getTotal()).toBe(0);
});
});
});
π Conclusion
Congratulations! π Youβve mastered the art of testing TypeScript applications with Jest. You now have the skills to:
- β Configure Jest for seamless TypeScript integration
- π§ͺ Write comprehensive tests with complete type safety
- π Create sophisticated mocks that maintain type checking
- π Measure code coverage and enforce quality standards
- ποΈ Build professional testing workflows that scale with your team
Youβve learned to test everything from simple utilities to complex business logic, ensuring your TypeScript applications are bulletproof and maintainable.
Keep practicing these patterns, and youβll become the testing champion your team needs! π Happy testing! π
π Next Steps
Ready to level up further? Check out these advanced topics:
- π Integration Testing with Supertest and databases
- π End-to-End Testing with Cypress and Playwright
- π Performance Testing and benchmarking
- π€ Test Automation in CI/CD pipelines
- π§ͺ Test-Driven Development (TDD) practices