Advanced JavaScriptFeatured
JavaScript Testing: Complete Guide to Unit and Integration Tests
Master JavaScript testing with Jest, Mocha, and testing best practices. Learn unit testing, integration testing, mocking, and TDD.
By JavaScriptDoc Team•
testingjestmochatddjavascript
JavaScript Testing: Complete Guide to Unit and Integration Tests
Testing is essential for building reliable JavaScript applications. This guide covers testing frameworks, methodologies, and best practices.
Introduction to Testing
Why Test?
// Without tests - risky refactoring
function calculateDiscount(price, customerType) {
if (customerType === 'premium') {
return price * 0.8; // 20% discount
}
return price;
}
// With tests - confident refactoring
function calculateDiscount(price, customerType, seasonalPromo = false) {
let discount = 0;
if (customerType === 'premium') {
discount = 0.2;
} else if (customerType === 'regular' && seasonalPromo) {
discount = 0.1;
}
return price * (1 - discount);
}
// Tests ensure behavior is preserved
describe('calculateDiscount', () => {
test('applies 20% discount for premium customers', () => {
expect(calculateDiscount(100, 'premium')).toBe(80);
});
test('no discount for regular customers without promo', () => {
expect(calculateDiscount(100, 'regular')).toBe(100);
});
test('applies 10% discount for regular customers with promo', () => {
expect(calculateDiscount(100, 'regular', true)).toBe(90);
});
});
Unit Testing Basics
Writing Your First Tests
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// math.test.js
import { add, multiply, divide } from './math';
describe('Math functions', () => {
describe('add', () => {
test('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('adds zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('multiply', () => {
test('multiplies two numbers', () => {
expect(multiply(3, 4)).toBe(12);
});
test('multiplies by zero', () => {
expect(multiply(5, 0)).toBe(0);
});
});
describe('divide', () => {
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws error on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
test('handles decimal results', () => {
expect(divide(7, 2)).toBe(3.5);
});
});
});
Test Structure and Assertions
// Common test structure
describe('Component/Module name', () => {
// Setup and teardown
beforeAll(() => {
// Run once before all tests
});
afterAll(() => {
// Run once after all tests
});
beforeEach(() => {
// Run before each test
});
afterEach(() => {
// Run after each test
});
// Group related tests
describe('feature or method', () => {
test('should do something', () => {
// Arrange
const input = 'test';
// Act
const result = functionUnderTest(input);
// Assert
expect(result).toBe('expected');
});
it('is an alias for test', () => {
expect(true).toBe(true);
});
});
});
// Common assertions
test('common assertions', () => {
// Equality
expect(2 + 2).toBe(4);
expect({ name: 'John' }).toEqual({ name: 'John' });
// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('Hello').toBeDefined();
// Numbers
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(0.1 + 0.2).toBeCloseTo(0.3);
// Strings
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');
// Arrays
expect(['Alice', 'Bob', 'Eve']).toContain('Alice');
expect([1, 2, 3]).toHaveLength(3);
// Objects
expect({ name: 'John', age: 30 }).toHaveProperty('name');
expect({ name: 'John', age: 30 }).toMatchObject({ name: 'John' });
// Exceptions
expect(() => {
throw new Error('Wrong!');
}).toThrow();
expect(() => {
throw new Error('Wrong!');
}).toThrow('Wrong');
});
Testing Asynchronous Code
Promises and Async/Await
// api.js
export async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
export function fetchUserData(id) {
return fetch(`/api/users/${id}`).then((response) => {
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
});
}
// api.test.js
import { fetchUser, fetchUserData } from './api';
// Testing with async/await
describe('fetchUser', () => {
test('fetches user successfully', async () => {
const user = await fetchUser(1);
expect(user).toEqual({
id: 1,
name: 'John Doe',
});
});
test('throws error for non-existent user', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
});
// Testing with promises
describe('fetchUserData', () => {
test('fetches user successfully', () => {
return fetchUserData(1).then((user) => {
expect(user).toEqual({
id: 1,
name: 'John Doe',
});
});
});
test('rejects for non-existent user', () => {
return expect(fetchUserData(999)).rejects.toThrow('User not found');
});
});
// Testing callbacks
function fetchDataCallback(callback) {
setTimeout(() => {
callback(null, { data: 'test' });
}, 100);
}
test('callback test with done', (done) => {
fetchDataCallback((error, data) => {
expect(error).toBeNull();
expect(data).toEqual({ data: 'test' });
done();
});
});
Testing Timers
// timer.js
export function delayedGreeting(name, callback) {
setTimeout(() => {
callback(`Hello, ${name}!`);
}, 1000);
}
export class Debouncer {
constructor(fn, delay) {
this.fn = fn;
this.delay = delay;
this.timeoutId = null;
}
call(...args) {
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => {
this.fn(...args);
}, this.delay);
}
}
// timer.test.js
import { delayedGreeting, Debouncer } from './timer';
// Using fake timers
describe('Timer functions', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('delayedGreeting calls callback after 1 second', () => {
const callback = jest.fn();
delayedGreeting('John', callback);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledWith('Hello, John!');
expect(callback).toHaveBeenCalledTimes(1);
});
test('Debouncer delays function calls', () => {
const fn = jest.fn();
const debouncer = new Debouncer(fn, 500);
debouncer.call('first');
debouncer.call('second');
debouncer.call('third');
jest.advanceTimersByTime(400);
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith('third');
});
});
Mocking and Test Doubles
Mock Functions
// userService.js
export class UserService {
constructor(database) {
this.database = database;
}
async getUser(id) {
const user = await this.database.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
async createUser(userData) {
const user = await this.database.create(userData);
await this.sendWelcomeEmail(user.email);
return user;
}
async sendWelcomeEmail(email) {
// Send email implementation
}
}
// userService.test.js
import { UserService } from './userService';
describe('UserService', () => {
let mockDatabase;
let userService;
beforeEach(() => {
// Create mock database
mockDatabase = {
findById: jest.fn(),
create: jest.fn(),
};
userService = new UserService(mockDatabase);
});
describe('getUser', () => {
test('returns user when found', async () => {
const mockUser = { id: 1, name: 'John' };
mockDatabase.findById.mockResolvedValue(mockUser);
const user = await userService.getUser(1);
expect(user).toEqual(mockUser);
expect(mockDatabase.findById).toHaveBeenCalledWith(1);
expect(mockDatabase.findById).toHaveBeenCalledTimes(1);
});
test('throws error when user not found', async () => {
mockDatabase.findById.mockResolvedValue(null);
await expect(userService.getUser(999)).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
test('creates user and sends welcome email', async () => {
const userData = { name: 'Jane', email: 'jane@example.com' };
const createdUser = { id: 2, ...userData };
mockDatabase.create.mockResolvedValue(createdUser);
userService.sendWelcomeEmail = jest.fn().mockResolvedValue();
const result = await userService.createUser(userData);
expect(result).toEqual(createdUser);
expect(mockDatabase.create).toHaveBeenCalledWith(userData);
expect(userService.sendWelcomeEmail).toHaveBeenCalledWith(
'jane@example.com'
);
});
});
});
Module Mocking
// emailService.js
export function sendEmail(to, subject, body) {
// Actual email sending logic
console.log(`Sending email to ${to}`);
}
// notifications.js
import { sendEmail } from './emailService';
export class NotificationService {
async notifyUser(user, message) {
const subject = 'New Notification';
const body = `Hello ${user.name},\n\n${message}`;
await sendEmail(user.email, subject, body);
return {
success: true,
recipient: user.email,
};
}
}
// notifications.test.js
import { NotificationService } from './notifications';
import { sendEmail } from './emailService';
// Mock the entire module
jest.mock('./emailService');
describe('NotificationService', () => {
let notificationService;
beforeEach(() => {
notificationService = new NotificationService();
// Clear mock data between tests
jest.clearAllMocks();
});
test('sends notification email', async () => {
const user = { name: 'John', email: 'john@example.com' };
const message = 'You have a new message';
// Mock implementation
sendEmail.mockResolvedValue();
const result = await notificationService.notifyUser(user, message);
expect(sendEmail).toHaveBeenCalledWith(
'john@example.com',
'New Notification',
expect.stringContaining('Hello John')
);
expect(result).toEqual({
success: true,
recipient: 'john@example.com',
});
});
});
Spies and Stubs
// Using spies to track calls
class Calculator {
add(a, b) {
return a + b;
}
multiply(a, b) {
return a * b;
}
calculate(operation, a, b) {
switch (operation) {
case 'add':
return this.add(a, b);
case 'multiply':
return this.multiply(a, b);
default:
throw new Error('Unknown operation');
}
}
}
test('spies on method calls', () => {
const calculator = new Calculator();
// Create a spy
const addSpy = jest.spyOn(calculator, 'add');
const result = calculator.calculate('add', 2, 3);
expect(result).toBe(5);
expect(addSpy).toHaveBeenCalledWith(2, 3);
expect(addSpy).toHaveBeenCalledTimes(1);
// Restore original implementation
addSpy.mockRestore();
});
// Creating stubs
test('stubs method implementation', () => {
const calculator = new Calculator();
// Stub the add method
jest.spyOn(calculator, 'add').mockReturnValue(10);
const result = calculator.calculate('add', 2, 3);
expect(result).toBe(10); // Stubbed value, not 5
});
Integration Testing
Testing API Endpoints
// server.js
import express from 'express';
export function createApp(database) {
const app = express();
app.use(express.json());
app.get('/api/users/:id', async (req, res) => {
try {
const user = await database.getUser(req.params.id);
res.json(user);
} catch (error) {
res.status(404).json({ error: 'User not found' });
}
});
app.post('/api/users', async (req, res) => {
try {
const user = await database.createUser(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
return app;
}
// server.test.js
import request from 'supertest';
import { createApp } from './server';
describe('API Integration Tests', () => {
let app;
let mockDatabase;
beforeEach(() => {
mockDatabase = {
getUser: jest.fn(),
createUser: jest.fn(),
};
app = createApp(mockDatabase);
});
describe('GET /api/users/:id', () => {
test('returns user when found', async () => {
const mockUser = { id: 1, name: 'John' };
mockDatabase.getUser.mockResolvedValue(mockUser);
const response = await request(app).get('/api/users/1').expect(200);
expect(response.body).toEqual(mockUser);
});
test('returns 404 when user not found', async () => {
mockDatabase.getUser.mockRejectedValue(new Error('Not found'));
const response = await request(app).get('/api/users/999').expect(404);
expect(response.body).toEqual({ error: 'User not found' });
});
});
describe('POST /api/users', () => {
test('creates new user', async () => {
const userData = { name: 'Jane', email: 'jane@example.com' };
const createdUser = { id: 2, ...userData };
mockDatabase.createUser.mockResolvedValue(createdUser);
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toEqual(createdUser);
expect(mockDatabase.createUser).toHaveBeenCalledWith(userData);
});
});
});
Database Integration Tests
// userRepository.js
export class UserRepository {
constructor(db) {
this.db = db;
}
async create(userData) {
const [id] = await this.db('users').insert(userData).returning('id');
return this.findById(id);
}
async findById(id) {
return this.db('users').where({ id }).first();
}
async findByEmail(email) {
return this.db('users').where({ email }).first();
}
async update(id, updates) {
await this.db('users').where({ id }).update(updates);
return this.findById(id);
}
async delete(id) {
return this.db('users').where({ id }).delete();
}
}
// userRepository.integration.test.js
import knex from 'knex';
import { UserRepository } from './userRepository';
describe('UserRepository Integration Tests', () => {
let db;
let userRepository;
beforeAll(async () => {
// Setup test database
db = knex({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
// Create tables
await db.schema.createTable('users', (table) => {
table.increments('id');
table.string('name');
table.string('email').unique();
table.timestamps(true, true);
});
userRepository = new UserRepository(db);
});
afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
// Clean data before each test
await db('users').truncate();
});
test('creates a new user', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const user = await userRepository.create(userData);
expect(user).toMatchObject(userData);
expect(user.id).toBeDefined();
});
test('finds user by id', async () => {
const created = await userRepository.create({
name: 'Jane',
email: 'jane@example.com',
});
const found = await userRepository.findById(created.id);
expect(found).toEqual(created);
});
test('updates user', async () => {
const user = await userRepository.create({
name: 'Bob',
email: 'bob@example.com',
});
const updated = await userRepository.update(user.id, { name: 'Robert' });
expect(updated.name).toBe('Robert');
expect(updated.email).toBe('bob@example.com');
});
});
Test-Driven Development (TDD)
TDD Workflow
// Step 1: Write failing test
describe('StringCalculator', () => {
test('returns 0 for empty string', () => {
const calculator = new StringCalculator();
expect(calculator.add('')).toBe(0);
});
});
// Step 2: Write minimal code to pass
class StringCalculator {
add(numbers) {
return 0;
}
}
// Step 3: Write next failing test
test('returns number for single number', () => {
const calculator = new StringCalculator();
expect(calculator.add('5')).toBe(5);
});
// Step 4: Update implementation
class StringCalculator {
add(numbers) {
if (numbers === '') return 0;
return parseInt(numbers);
}
}
// Step 5: Continue cycle
test('adds two numbers separated by comma', () => {
const calculator = new StringCalculator();
expect(calculator.add('1,2')).toBe(3);
});
// Final implementation after multiple cycles
class StringCalculator {
add(numbers) {
if (numbers === '') return 0;
const delimiter = /[,\n]/;
const nums = numbers.split(delimiter);
return nums.reduce((sum, num) => {
const value = parseInt(num) || 0;
if (value < 0) {
throw new Error(`Negative numbers not allowed: ${value}`);
}
return sum + (value > 1000 ? 0 : value);
}, 0);
}
}
// Complete test suite
describe('StringCalculator', () => {
let calculator;
beforeEach(() => {
calculator = new StringCalculator();
});
test('returns 0 for empty string', () => {
expect(calculator.add('')).toBe(0);
});
test('returns number for single number', () => {
expect(calculator.add('5')).toBe(5);
});
test('adds two numbers separated by comma', () => {
expect(calculator.add('1,2')).toBe(3);
});
test('adds multiple numbers', () => {
expect(calculator.add('1,2,3,4')).toBe(10);
});
test('handles newline delimiter', () => {
expect(calculator.add('1\n2,3')).toBe(6);
});
test('throws on negative numbers', () => {
expect(() => calculator.add('1,-2')).toThrow(
'Negative numbers not allowed: -2'
);
});
test('ignores numbers greater than 1000', () => {
expect(calculator.add('2,1001')).toBe(2);
});
});
Testing Best Practices
Test Organization
// Good test organization
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart();
});
describe('addItem', () => {
test('adds item to empty cart', () => {
const item = { id: 1, name: 'Book', price: 10 };
cart.addItem(item);
expect(cart.getItems()).toHaveLength(1);
expect(cart.getItems()[0]).toEqual({ ...item, quantity: 1 });
});
test('increases quantity for existing item', () => {
const item = { id: 1, name: 'Book', price: 10 };
cart.addItem(item);
cart.addItem(item);
expect(cart.getItems()).toHaveLength(1);
expect(cart.getItems()[0].quantity).toBe(2);
});
});
describe('removeItem', () => {
test('removes item from cart', () => {
const item = { id: 1, name: 'Book', price: 10 };
cart.addItem(item);
cart.removeItem(1);
expect(cart.getItems()).toHaveLength(0);
});
test('does nothing if item not in cart', () => {
expect(() => cart.removeItem(999)).not.toThrow();
});
});
describe('calculateTotal', () => {
test('returns 0 for empty cart', () => {
expect(cart.calculateTotal()).toBe(0);
});
test('calculates total with multiple items', () => {
cart.addItem({ id: 1, name: 'Book', price: 10 });
cart.addItem({ id: 2, name: 'Pen', price: 5 });
cart.addItem({ id: 1, name: 'Book', price: 10 }); // quantity 2
expect(cart.calculateTotal()).toBe(25);
});
});
});
Test Data Builders
// Test data builder pattern
class UserBuilder {
constructor() {
this.user = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30,
isActive: true,
};
}
withId(id) {
this.user.id = id;
return this;
}
withName(name) {
this.user.name = name;
return this;
}
withEmail(email) {
this.user.email = email;
return this;
}
withAge(age) {
this.user.age = age;
return this;
}
inactive() {
this.user.isActive = false;
return this;
}
build() {
return { ...this.user };
}
}
// Usage in tests
test('filters active users', () => {
const users = [
new UserBuilder().withId(1).build(),
new UserBuilder().withId(2).inactive().build(),
new UserBuilder().withId(3).build(),
];
const activeUsers = filterActiveUsers(users);
expect(activeUsers).toHaveLength(2);
expect(activeUsers.map((u) => u.id)).toEqual([1, 3]);
});
// Factory functions
function createUser(overrides = {}) {
return {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30,
isActive: true,
...overrides,
};
}
test('user validation', () => {
const validUser = createUser();
const invalidUser = createUser({ email: 'invalid' });
expect(validateUser(validUser)).toBe(true);
expect(validateUser(invalidUser)).toBe(false);
});
Testing Error Scenarios
// Comprehensive error testing
class PaymentProcessor {
constructor(gateway) {
this.gateway = gateway;
}
async processPayment(amount, cardNumber) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
if (!this.validateCard(cardNumber)) {
throw new Error('Invalid card number');
}
try {
const result = await this.gateway.charge(amount, cardNumber);
return {
success: true,
transactionId: result.id,
};
} catch (error) {
if (error.code === 'INSUFFICIENT_FUNDS') {
throw new Error('Payment declined: insufficient funds');
}
throw new Error('Payment processing failed');
}
}
validateCard(cardNumber) {
return /^\d{16}$/.test(cardNumber);
}
}
describe('PaymentProcessor error handling', () => {
let processor;
let mockGateway;
beforeEach(() => {
mockGateway = {
charge: jest.fn(),
};
processor = new PaymentProcessor(mockGateway);
});
test('throws error for negative amount', async () => {
await expect(
processor.processPayment(-10, '1234567890123456')
).rejects.toThrow('Amount must be positive');
expect(mockGateway.charge).not.toHaveBeenCalled();
});
test('throws error for invalid card', async () => {
await expect(processor.processPayment(100, '1234')).rejects.toThrow(
'Invalid card number'
);
});
test('handles insufficient funds', async () => {
mockGateway.charge.mockRejectedValue({
code: 'INSUFFICIENT_FUNDS',
});
await expect(
processor.processPayment(100, '1234567890123456')
).rejects.toThrow('Payment declined: insufficient funds');
});
test('handles generic gateway errors', async () => {
mockGateway.charge.mockRejectedValue(new Error('Network error'));
await expect(
processor.processPayment(100, '1234567890123456')
).rejects.toThrow('Payment processing failed');
});
});
Coverage and Quality
// Example of good test coverage
class TodoList {
constructor() {
this.todos = [];
this.nextId = 1;
}
addTodo(text) {
if (!text || text.trim() === '') {
throw new Error('Todo text cannot be empty');
}
const todo = {
id: this.nextId++,
text: text.trim(),
completed: false,
createdAt: new Date(),
};
this.todos.push(todo);
return todo;
}
toggleTodo(id) {
const todo = this.todos.find((t) => t.id === id);
if (!todo) {
throw new Error('Todo not found');
}
todo.completed = !todo.completed;
return todo;
}
deleteTodo(id) {
const index = this.todos.findIndex((t) => t.id === id);
if (index === -1) {
throw new Error('Todo not found');
}
return this.todos.splice(index, 1)[0];
}
getActiveTodos() {
return this.todos.filter((t) => !t.completed);
}
getCompletedTodos() {
return this.todos.filter((t) => t.completed);
}
}
// Comprehensive test suite
describe('TodoList - 100% coverage', () => {
let todoList;
beforeEach(() => {
todoList = new TodoList();
});
describe('addTodo', () => {
test('adds todo with valid text', () => {
const todo = todoList.addTodo('Test todo');
expect(todo).toMatchObject({
id: 1,
text: 'Test todo',
completed: false,
});
expect(todo.createdAt).toBeInstanceOf(Date);
});
test('trims whitespace', () => {
const todo = todoList.addTodo(' Test todo ');
expect(todo.text).toBe('Test todo');
});
test('throws on empty text', () => {
expect(() => todoList.addTodo('')).toThrow('Todo text cannot be empty');
expect(() => todoList.addTodo(' ')).toThrow(
'Todo text cannot be empty'
);
expect(() => todoList.addTodo(null)).toThrow('Todo text cannot be empty');
});
test('increments id', () => {
const todo1 = todoList.addTodo('First');
const todo2 = todoList.addTodo('Second');
expect(todo1.id).toBe(1);
expect(todo2.id).toBe(2);
});
});
describe('toggleTodo', () => {
test('toggles todo completion', () => {
const todo = todoList.addTodo('Test');
todoList.toggleTodo(todo.id);
expect(todo.completed).toBe(true);
todoList.toggleTodo(todo.id);
expect(todo.completed).toBe(false);
});
test('throws on non-existent todo', () => {
expect(() => todoList.toggleTodo(999)).toThrow('Todo not found');
});
});
describe('deleteTodo', () => {
test('deletes existing todo', () => {
const todo = todoList.addTodo('Test');
const deleted = todoList.deleteTodo(todo.id);
expect(deleted).toEqual(todo);
expect(todoList.todos).toHaveLength(0);
});
test('throws on non-existent todo', () => {
expect(() => todoList.deleteTodo(999)).toThrow('Todo not found');
});
});
describe('filtering', () => {
beforeEach(() => {
todoList.addTodo('Active 1');
const completed = todoList.addTodo('Completed');
todoList.toggleTodo(completed.id);
todoList.addTodo('Active 2');
});
test('getActiveTodos returns only active', () => {
const active = todoList.getActiveTodos();
expect(active).toHaveLength(2);
expect(active.every((t) => !t.completed)).toBe(true);
});
test('getCompletedTodos returns only completed', () => {
const completed = todoList.getCompletedTodos();
expect(completed).toHaveLength(1);
expect(completed.every((t) => t.completed)).toBe(true);
});
});
});
Best Practices
-
Write descriptive test names
// Bad test('test 1', () => {}); // Good test('should return user name when user exists', () => {});
-
Follow AAA pattern
test('calculates order total with tax', () => { // Arrange const items = [{ price: 10 }, { price: 20 }]; const taxRate = 0.1; // Act const total = calculateTotal(items, taxRate); // Assert expect(total).toBe(33); });
-
Test behavior, not implementation
// Bad - testing implementation test('uses reduce to sum numbers', () => { const spy = jest.spyOn(Array.prototype, 'reduce'); sum([1, 2, 3]); expect(spy).toHaveBeenCalled(); }); // Good - testing behavior test('sums array of numbers', () => { expect(sum([1, 2, 3])).toBe(6); });
-
Keep tests independent
// Each test should be able to run in isolation beforeEach(() => { // Reset state }); afterEach(() => { // Clean up });
Conclusion
Testing is crucial for JavaScript application quality:
- Unit tests for individual components
- Integration tests for component interactions
- Mocking for isolating dependencies
- TDD for design-driven development
- Coverage for identifying untested code
- Best practices for maintainable tests
Key takeaways:
- Test behavior, not implementation
- Keep tests simple and focused
- Use appropriate test doubles
- Maintain good test coverage
- Follow consistent patterns
- Run tests frequently
Master testing to build reliable, maintainable JavaScript applications!