It was my second week at Microsoft, and I had just pushed what I thought was a simple bug fix to production. Within minutes, our main dashboard was broken, user authentication was failing, and notifications had stopped working. I'd changed one function, but somehow broke three unrelated features.
My team lead sat me down with a coffee and said, "Maya, let me tell you about the most valuable skill you'll learn as a developer - how to write tests that catch these bugs before they reach production." That afternoon, he introduced me to unit testing, and it completely transformed how I write code.
Six months later, our team had the lowest bug rate in the entire organization. We deployed multiple times per day with confidence, and the phrase "works on my machine" became obsolete. Unit testing didn't just catch bugs - it made me a better developer by forcing me to think about edge cases and design more modular code.
Table Of Contents
- The Untested Code That Broke Everything
- Your First Unit Test
- Testing Fundamentals: The AAA Pattern
- Jest Matchers and Utilities
- Mocking: Isolating Units
- Test-Driven Development (TDD)
- Async Testing Patterns
- Test Coverage and Quality
- Integration Testing
- Testing React Components
- Performance Testing
- Test Organization and Best Practices
- Continuous Integration Setup
- Final Thoughts: Tests as Documentation
The Untested Code That Broke Everything
Here's the "simple" function that brought down our dashboard:
// The function that looked innocent but hid explosive bugs
class UserManager {
constructor(database, cache, emailService) {
this.db = database;
this.cache = cache;
this.emailService = emailService;
}
// The seemingly simple function that broke everything
async updateUserProfile(userId, updates) {
// Bug 1: No input validation
const user = await this.db.findUser(userId);
// Bug 2: Null pointer waiting to happen
user.firstName = updates.firstName;
user.lastName = updates.lastName;
user.email = updates.email;
// Bug 3: No email validation
if (user.email !== user.previousEmail) {
await this.emailService.sendVerificationEmail(user.email);
}
// Bug 4: Cache invalidation happens before DB save
await this.cache.delete(`user:${userId}`);
// Bug 5: No error handling for DB failure
await this.db.updateUser(userId, user);
// Bug 6: Return value inconsistency
return user.id ? true : user;
}
// The "fix" that broke authentication
async getUserById(userId) {
// My "optimization" that killed performance
if (!userId || userId.length < 3) {
return null; // BUG: UUIDs are 36 chars, numeric IDs are shorter!
}
const cached = await this.cache.get(`user:${userId}`);
if (cached) {
return JSON.parse(cached);
}
const user = await this.db.findUser(userId);
if (user) {
await this.cache.set(`user:${userId}`, JSON.stringify(user));
}
return user;
}
}
// What went wrong in production:
// 1. updateUserProfile(null, {}) → Null pointer exception
// 2. getUserById("42") → Returned null for valid numeric IDs
// 3. Invalid emails triggered spam detection
// 4. Cache corruption from failed DB saves
// 5. Dashboard couldn't load users with numeric IDs
Your First Unit Test
Let me show you how testing would have caught these bugs:
// Essential test setup
const { UserManager } = require('../src/UserManager');
const { MockDatabase, MockCache, MockEmailService } = require('./mocks');
describe('UserManager', () => {
let userManager;
let mockDb;
let mockCache;
let mockEmailService;
beforeEach(() => {
// Fresh mocks for each test
mockDb = new MockDatabase();
mockCache = new MockCache();
mockEmailService = new MockEmailService();
userManager = new UserManager(mockDb, mockCache, mockEmailService);
});
describe('updateUserProfile', () => {
test('should update user profile successfully', async () => {
// Arrange - Set up test data
const userId = 'user-123';
const existingUser = {
id: 'user-123',
firstName: 'Maya',
lastName: 'Chen',
email: 'maya@oldmail.com',
previousEmail: 'maya@oldmail.com'
};
const updates = {
firstName: 'Maya',
lastName: 'Chen-Smith',
email: 'maya@newmail.com'
};
mockDb.findUser.mockResolvedValue(existingUser);
mockDb.updateUser.mockResolvedValue(true);
mockEmailService.sendVerificationEmail.mockResolvedValue(true);
// Act - Execute the function
const result = await userManager.updateUserProfile(userId, updates);
// Assert - Verify the results
expect(result).toBe(true);
expect(mockDb.updateUser).toHaveBeenCalledWith(userId, {
...existingUser,
firstName: 'Maya',
lastName: 'Chen-Smith',
email: 'maya@newmail.com'
});
expect(mockEmailService.sendVerificationEmail)
.toHaveBeenCalledWith('maya@newmail.com');
expect(mockCache.delete).toHaveBeenCalledWith(`user:${userId}`);
});
test('should handle null user gracefully', async () => {
// This test would have caught Bug 1!
mockDb.findUser.mockResolvedValue(null);
await expect(userManager.updateUserProfile('user-123', {}))
.rejects.toThrow('User not found');
});
test('should validate email format', async () => {
// This would have caught Bug 3!
const existingUser = { id: 'user-123', email: 'old@email.com' };
const updates = { email: 'invalid-email' };
mockDb.findUser.mockResolvedValue(existingUser);
await expect(userManager.updateUserProfile('user-123', updates))
.rejects.toThrow('Invalid email format');
});
test('should not invalidate cache if database update fails', async () => {
// This would have caught Bug 4!
const existingUser = { id: 'user-123', email: 'old@email.com' };
mockDb.findUser.mockResolvedValue(existingUser);
mockDb.updateUser.mockRejectedValue(new Error('DB Error'));
await expect(userManager.updateUserProfile('user-123', {}))
.rejects.toThrow('DB Error');
// Cache should not be deleted if DB update failed
expect(mockCache.delete).not.toHaveBeenCalled();
});
});
describe('getUserById', () => {
test('should handle numeric user IDs', async () => {
// This would have caught Bug 2!
const userId = '42';
const user = { id: '42', name: 'Alice' };
mockCache.get.mockResolvedValue(null);
mockDb.findUser.mockResolvedValue(user);
const result = await userManager.getUserById(userId);
expect(result).toEqual(user);
expect(mockDb.findUser).toHaveBeenCalledWith(userId);
});
test('should return null for invalid user IDs', async () => {
const result = await userManager.getUserById('');
expect(result).toBeNull();
const result2 = await userManager.getUserById(null);
expect(result2).toBeNull();
});
test('should use cache when available', async () => {
const userId = 'user-123';
const cachedUser = { id: 'user-123', name: 'Cached User' };
mockCache.get.mockResolvedValue(JSON.stringify(cachedUser));
const result = await userManager.getUserById(userId);
expect(result).toEqual(cachedUser);
expect(mockDb.findUser).not.toHaveBeenCalled();
});
});
});
Testing Fundamentals: The AAA Pattern
// The Arrange-Act-Assert pattern
describe('Calculator', () => {
test('should add two numbers correctly', () => {
// ARRANGE - Set up test data and conditions
const calculator = new Calculator();
const firstNumber = 5;
const secondNumber = 3;
const expectedResult = 8;
// ACT - Execute the function being tested
const result = calculator.add(firstNumber, secondNumber);
// ASSERT - Verify the results match expectations
expect(result).toBe(expectedResult);
expect(result).toBeGreaterThan(0);
expect(typeof result).toBe('number');
});
test('should handle edge cases', () => {
const calculator = new Calculator();
// Test zero
expect(calculator.add(0, 0)).toBe(0);
// Test negative numbers
expect(calculator.add(-5, 3)).toBe(-2);
// Test floating point
expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3, 5);
// Test large numbers
expect(calculator.add(Number.MAX_SAFE_INTEGER, 1))
.toBeGreaterThan(Number.MAX_SAFE_INTEGER);
});
test('should throw error for invalid inputs', () => {
const calculator = new Calculator();
// Test null inputs
expect(() => calculator.add(null, 5))
.toThrow('Invalid input: numbers required');
// Test string inputs
expect(() => calculator.add('5', 3))
.toThrow('Invalid input: numbers required');
// Test undefined inputs
expect(() => calculator.add(undefined, 5))
.toThrow('Invalid input: numbers required');
});
});
Jest Matchers and Utilities
// Comprehensive Jest matcher examples
describe('Jest Matchers', () => {
// Equality matchers
test('equality matchers', () => {
expect(2 + 2).toBe(4); // Exact equality (===)
expect({ name: 'Maya' }).toEqual({ name: 'Maya' }); // Deep equality
expect([1, 2, 3]).toStrictEqual([1, 2, 3]); // Strict equality
});
// Truthiness matchers
test('truthiness matchers', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('Hello').toBeDefined();
});
// Number matchers
test('number matchers', () => {
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(2 + 2).toBeLessThanOrEqual(4);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});
// String matchers
test('string matchers', () => {
expect('Hello World').toMatch(/World/);
expect('maya@coffee.dev').toMatch(/^[\w.]+@[\w.]+\.[a-z]+$/);
expect('React Testing Library').toContain('Testing');
});
// Array matchers
test('array matchers', () => {
const fruits = ['apple', 'banana', 'orange'];
expect(fruits).toContain('banana');
expect(fruits).toHaveLength(3);
expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['a', 'c']));
});
// Object matchers
test('object matchers', () => {
const user = {
id: 1,
name: 'Maya',
email: 'maya@coffee.dev',
profile: { age: 28, city: 'Seattle' }
};
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('profile.city', 'Seattle');
expect(user).toMatchObject({
name: 'Maya',
email: expect.stringContaining('@coffee.dev')
});
});
// Exception matchers
test('exception matchers', () => {
const throwError = () => {
throw new Error('Something went wrong');
};
expect(throwError).toThrow();
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(/went wrong/);
expect(throwError).toThrow(Error);
});
// Promise matchers
test('promise matchers', async () => {
const promise = Promise.resolve('success');
const rejectedPromise = Promise.reject('error');
await expect(promise).resolves.toBe('success');
await expect(rejectedPromise).rejects.toBe('error');
// Alternative syntax
await expect(promise).resolves.toMatch(/success/);
});
});
Mocking: Isolating Units
// Advanced mocking techniques
describe('Advanced Mocking', () => {
// Basic function mocking
test('should mock function calls', () => {
const mockCallback = jest.fn();
[1, 2, 3].forEach(mockCallback);
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenLastCalledWith(3);
expect(mockCallback.mock.calls).toEqual([[1], [2], [3]]);
});
// Mock return values
test('should mock return values', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(10).mockReturnValue(20);
expect(mockFn()).toBe(10);
expect(mockFn()).toBe(20);
expect(mockFn()).toBe(20);
});
// Mock implementations
test('should mock implementations', () => {
const mockFn = jest.fn(x => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
// Change implementation
mockFn.mockImplementation(x => x + 1);
expect(mockFn(5)).toBe(6);
});
// Mock modules
test('should mock modules', () => {
// Mock axios module
jest.mock('axios');
const axios = require('axios');
axios.get.mockResolvedValue({
data: { id: 1, name: 'Maya' }
});
// Test code that uses axios
return axios.get('/api/users/1').then(response => {
expect(response.data.name).toBe('Maya');
});
});
// Partial mocking
test('should partially mock modules', () => {
jest.mock('../utils', () => ({
...jest.requireActual('../utils'),
getCurrentTime: jest.fn(() => '2024-01-01T00:00:00Z')
}));
const utils = require('../utils');
expect(utils.getCurrentTime()).toBe('2024-01-01T00:00:00Z');
// Other utils functions work normally
});
// Spy on methods
test('should spy on methods', () => {
const user = {
getName: () => 'Maya',
setName: (name) => { this.name = name; }
};
const spy = jest.spyOn(user, 'getName');
expect(user.getName()).toBe('Maya');
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // Restore original implementation
});
});
// Real-world mocking example
class APIService {
constructor(httpClient) {
this.httpClient = httpClient;
}
async getUser(id) {
const response = await this.httpClient.get(`/users/${id}`);
return response.data;
}
async createUser(userData) {
const response = await this.httpClient.post('/users', userData);
return response.data;
}
}
describe('APIService', () => {
let apiService;
let mockHttpClient;
beforeEach(() => {
mockHttpClient = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
};
apiService = new APIService(mockHttpClient);
});
test('should get user by ID', async () => {
const userId = 'user-123';
const userData = { id: userId, name: 'Maya' };
mockHttpClient.get.mockResolvedValue({ data: userData });
const result = await apiService.getUser(userId);
expect(result).toEqual(userData);
expect(mockHttpClient.get).toHaveBeenCalledWith(`/users/${userId}`);
});
test('should handle API errors gracefully', async () => {
mockHttpClient.get.mockRejectedValue(new Error('Network error'));
await expect(apiService.getUser('123')).rejects.toThrow('Network error');
});
});
Test-Driven Development (TDD)
// TDD Cycle: Red → Green → Refactor
describe('TDD Example: Password Validator', () => {
// Step 1: Write failing test (RED)
test('should require minimum length', () => {
const validator = new PasswordValidator();
expect(validator.validate('123')).toBe(false);
expect(validator.getErrors()).toContain('Password must be at least 8 characters');
});
// Step 2: Write minimal code to pass (GREEN)
class PasswordValidator {
validate(password) {
this.errors = [];
if (password.length < 8) {
this.errors.push('Password must be at least 8 characters');
return false;
}
return true;
}
getErrors() {
return this.errors;
}
}
// Step 3: Add more tests and refactor
test('should require uppercase letter', () => {
const validator = new PasswordValidator();
expect(validator.validate('password123')).toBe(false);
expect(validator.getErrors()).toContain('Password must contain an uppercase letter');
});
test('should require lowercase letter', () => {
const validator = new PasswordValidator();
expect(validator.validate('PASSWORD123')).toBe(false);
expect(validator.getErrors()).toContain('Password must contain a lowercase letter');
});
test('should require number', () => {
const validator = new PasswordValidator();
expect(validator.validate('Password')).toBe(false);
expect(validator.getErrors()).toContain('Password must contain a number');
});
test('should require special character', () => {
const validator = new PasswordValidator();
expect(validator.validate('Password123')).toBe(false);
expect(validator.getErrors()).toContain('Password must contain a special character');
});
test('should accept valid password', () => {
const validator = new PasswordValidator();
expect(validator.validate('Password123!')).toBe(true);
expect(validator.getErrors()).toHaveLength(0);
});
});
// Refactored implementation
class PasswordValidator {
constructor() {
this.rules = [
{
test: (password) => password.length >= 8,
message: 'Password must be at least 8 characters'
},
{
test: (password) => /[A-Z]/.test(password),
message: 'Password must contain an uppercase letter'
},
{
test: (password) => /[a-z]/.test(password),
message: 'Password must contain a lowercase letter'
},
{
test: (password) => /\d/.test(password),
message: 'Password must contain a number'
},
{
test: (password) => /[!@#$%^&*]/.test(password),
message: 'Password must contain a special character'
}
];
}
validate(password) {
this.errors = [];
this.rules.forEach(rule => {
if (!rule.test(password)) {
this.errors.push(rule.message);
}
});
return this.errors.length === 0;
}
getErrors() {
return this.errors;
}
}
Async Testing Patterns
// Testing asynchronous code
describe('Async Testing', () => {
// Promise-based tests
test('should handle promises with return', () => {
return fetchUser('123').then(user => {
expect(user.name).toBe('Maya');
});
});
// Async/await tests
test('should handle async/await', async () => {
const user = await fetchUser('123');
expect(user.name).toBe('Maya');
});
// Testing rejected promises
test('should handle promise rejections', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
// Testing callbacks
test('should handle callbacks', (done) => {
fetchUserCallback('123', (error, user) => {
if (error) {
done(error);
return;
}
expect(user.name).toBe('Maya');
done();
});
});
// Testing timers
test('should handle timers', (done) => {
jest.useFakeTimers();
setTimeout(() => {
expect(true).toBe(true);
done();
}, 1000);
jest.runAllTimers();
});
// Testing intervals
test('should handle intervals', () => {
jest.useFakeTimers();
const callback = jest.fn();
setInterval(callback, 1000);
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
jest.clearAllTimers();
});
});
// Real-world async testing example
class NotificationService {
constructor(emailService, smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
async sendNotification(user, message, options = {}) {
const results = [];
if (options.email && user.email) {
try {
await this.emailService.send(user.email, message);
results.push({ type: 'email', status: 'success' });
} catch (error) {
results.push({ type: 'email', status: 'failed', error: error.message });
}
}
if (options.sms && user.phone) {
try {
await this.smsService.send(user.phone, message);
results.push({ type: 'sms', status: 'success' });
} catch (error) {
results.push({ type: 'sms', status: 'failed', error: error.message });
}
}
return results;
}
}
describe('NotificationService', () => {
let notificationService;
let mockEmailService;
let mockSmsService;
beforeEach(() => {
mockEmailService = { send: jest.fn() };
mockSmsService = { send: jest.fn() };
notificationService = new NotificationService(mockEmailService, mockSmsService);
});
test('should send email notification', async () => {
const user = { email: 'maya@coffee.dev' };
const message = 'Hello!';
mockEmailService.send.mockResolvedValue(true);
const results = await notificationService.sendNotification(
user,
message,
{ email: true }
);
expect(results).toEqual([{ type: 'email', status: 'success' }]);
expect(mockEmailService.send).toHaveBeenCalledWith('maya@coffee.dev', 'Hello!');
});
test('should handle email failures gracefully', async () => {
const user = { email: 'maya@coffee.dev' };
const message = 'Hello!';
mockEmailService.send.mockRejectedValue(new Error('SMTP Error'));
const results = await notificationService.sendNotification(
user,
message,
{ email: true }
);
expect(results).toEqual([{
type: 'email',
status: 'failed',
error: 'SMTP Error'
}]);
});
test('should send both email and SMS', async () => {
const user = { email: 'maya@coffee.dev', phone: '+1234567890' };
const message = 'Hello!';
mockEmailService.send.mockResolvedValue(true);
mockSmsService.send.mockResolvedValue(true);
const results = await notificationService.sendNotification(
user,
message,
{ email: true, sms: true }
);
expect(results).toHaveLength(2);
expect(results).toContainEqual({ type: 'email', status: 'success' });
expect(results).toContainEqual({ type: 'sms', status: 'success' });
});
});
Test Coverage and Quality
// Coverage configuration in jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/critical/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/**/*.test.{js,jsx}',
'!src/**/__tests__/**'
]
};
// Quality over quantity - testing the right things
describe('Coverage vs Quality', () => {
// BAD: High coverage but poor quality
test('bad test - just for coverage', () => {
const calculator = new Calculator();
calculator.add(1, 1); // No assertions!
calculator.subtract(2, 1);
calculator.multiply(2, 2);
calculator.divide(4, 2);
// 100% coverage but 0% value
});
// GOOD: Lower coverage but high value
test('good test - testing behavior', () => {
const calculator = new Calculator();
// Test the behavior, not just the code
expect(calculator.add(1, 1)).toBe(2);
expect(calculator.add(-1, 1)).toBe(0);
expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3);
// Test edge cases
expect(() => calculator.add(null, 1)).toThrow();
expect(() => calculator.add(Infinity, 1)).toThrow();
});
// Focus on critical paths
test('should test critical business logic', () => {
const orderService = new OrderService();
// Test the business rules that matter
const order = {
items: [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
],
shippingAddress: 'Seattle, WA',
discountCode: 'SAVE10'
};
const total = orderService.calculateTotal(order);
// This calculation affects revenue!
expect(total.subtotal).toBe(250);
expect(total.discount).toBe(25);
expect(total.shipping).toBe(15);
expect(total.tax).toBe(24);
expect(total.total).toBe(264);
});
});
Integration Testing
// Testing components together
describe('Integration Tests', () => {
let app;
let database;
beforeAll(async () => {
// Set up test database
database = new TestDatabase();
await database.connect();
// Start test server
app = createApp({ database });
});
afterAll(async () => {
await database.disconnect();
});
beforeEach(async () => {
// Clean slate for each test
await database.clear();
});
test('should create and retrieve user', async () => {
const userData = {
name: 'Maya Chen',
email: 'maya@coffee.dev'
};
// Create user
const createResponse = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
const userId = createResponse.body.id;
// Retrieve user
const getResponse = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(getResponse.body).toMatchObject(userData);
});
test('should handle authentication flow', async () => {
// Register user
await request(app)
.post('/api/auth/register')
.send({
email: 'maya@coffee.dev',
password: 'SecurePassword123!'
})
.expect(201);
// Login
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'maya@coffee.dev',
password: 'SecurePassword123!'
})
.expect(200);
const token = loginResponse.body.token;
// Access protected route
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
// Access without token should fail
await request(app)
.get('/api/profile')
.expect(401);
});
});
Testing React Components
// React component testing with Jest and React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from '../UserProfile';
describe('UserProfile Component', () => {
const mockUser = {
id: '123',
name: 'Maya Chen',
email: 'maya@coffee.dev',
avatar: 'https://example.com/avatar.jpg'
};
test('should render user information', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText('Maya Chen')).toBeInTheDocument();
expect(screen.getByText('maya@coffee.dev')).toBeInTheDocument();
expect(screen.getByAltText('Maya Chen avatar')).toHaveAttribute(
'src',
mockUser.avatar
);
});
test('should handle edit mode', async () => {
const mockOnSave = jest.fn();
render(<UserProfile user={mockUser} onSave={mockOnSave} />);
// Enter edit mode
fireEvent.click(screen.getByText('Edit'));
// Check edit form is visible
expect(screen.getByDisplayValue('Maya Chen')).toBeInTheDocument();
expect(screen.getByDisplayValue('maya@coffee.dev')).toBeInTheDocument();
// Change name
const nameInput = screen.getByDisplayValue('Maya Chen');
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'Maya Chen-Smith');
// Save changes
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith({
...mockUser,
name: 'Maya Chen-Smith'
});
});
});
test('should handle loading state', () => {
render(<UserProfile user={null} loading={true} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Maya Chen')).not.toBeInTheDocument();
});
test('should handle error state', () => {
const error = 'Failed to load user';
render(<UserProfile error={error} />);
expect(screen.getByText(error)).toBeInTheDocument();
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
Performance Testing
// Performance and benchmark testing
describe('Performance Tests', () => {
test('should complete search within time limit', async () => {
const searchService = new SearchService();
const largeDataset = generateTestData(100000);
const startTime = performance.now();
const results = await searchService.search(largeDataset, 'query');
const endTime = performance.now();
const duration = endTime - startTime;
expect(duration).toBeLessThan(1000); // Should complete within 1 second
expect(results).toBeDefined();
});
test('should handle memory efficiently', () => {
const initialMemory = process.memoryUsage().heapUsed;
const processor = new DataProcessor();
processor.processLargeDataset(generateTestData(50000));
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
// Memory increase should be reasonable
expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024); // 100MB
});
test('should handle concurrent requests', async () => {
const apiService = new APIService();
const concurrentRequests = 100;
const startTime = performance.now();
const promises = Array.from({ length: concurrentRequests }, (_, i) =>
apiService.getUser(`user-${i}`)
);
const results = await Promise.all(promises);
const endTime = performance.now();
const duration = endTime - startTime;
expect(results).toHaveLength(concurrentRequests);
expect(duration).toBeLessThan(5000); // Should handle 100 requests in 5 seconds
});
});
Test Organization and Best Practices
// Well-organized test structure
describe('OrderService', () => {
let orderService;
let mockDatabase;
let mockPaymentService;
let mockInventoryService;
beforeEach(() => {
mockDatabase = createMockDatabase();
mockPaymentService = createMockPaymentService();
mockInventoryService = createMockInventoryService();
orderService = new OrderService({
database: mockDatabase,
paymentService: mockPaymentService,
inventoryService: mockInventoryService
});
});
describe('createOrder', () => {
const validOrderData = {
userId: 'user-123',
items: [
{ productId: 'prod-1', quantity: 2, price: 100 },
{ productId: 'prod-2', quantity: 1, price: 50 }
],
shippingAddress: {
street: '123 Main St',
city: 'Seattle',
state: 'WA',
zip: '98101'
}
};
describe('when all conditions are met', () => {
beforeEach(() => {
mockInventoryService.checkAvailability.mockResolvedValue(true);
mockPaymentService.processPayment.mockResolvedValue({ success: true });
mockDatabase.saveOrder.mockResolvedValue({ id: 'order-123' });
});
test('should create order successfully', async () => {
const result = await orderService.createOrder(validOrderData);
expect(result).toMatchObject({
id: 'order-123',
status: 'confirmed',
total: 250
});
expect(mockInventoryService.checkAvailability).toHaveBeenCalled();
expect(mockPaymentService.processPayment).toHaveBeenCalled();
expect(mockDatabase.saveOrder).toHaveBeenCalled();
});
});
describe('when inventory is insufficient', () => {
beforeEach(() => {
mockInventoryService.checkAvailability.mockResolvedValue(false);
});
test('should throw inventory error', async () => {
await expect(orderService.createOrder(validOrderData))
.rejects.toThrow('Insufficient inventory');
expect(mockPaymentService.processPayment).not.toHaveBeenCalled();
});
});
describe('when payment fails', () => {
beforeEach(() => {
mockInventoryService.checkAvailability.mockResolvedValue(true);
mockPaymentService.processPayment.mockResolvedValue({
success: false,
error: 'Card declined'
});
});
test('should throw payment error', async () => {
await expect(orderService.createOrder(validOrderData))
.rejects.toThrow('Payment failed: Card declined');
expect(mockDatabase.saveOrder).not.toHaveBeenCalled();
});
});
});
describe('calculateShipping', () => {
test.each([
[0, 'WA', 0],
[50, 'WA', 5],
[100, 'WA', 10],
[50, 'CA', 15],
[100, 'NY', 20]
])('should calculate shipping for order total %i in %s as %i',
(orderTotal, state, expectedShipping) => {
const shipping = orderService.calculateShipping(orderTotal, state);
expect(shipping).toBe(expectedShipping);
});
});
});
// Test utilities and helpers
const TestUtils = {
createMockUser: (overrides = {}) => ({
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
...overrides
}),
createMockOrder: (overrides = {}) => ({
id: 'order-123',
userId: 'user-123',
total: 100,
status: 'pending',
createdAt: new Date().toISOString(),
...overrides
}),
waitFor: (fn, timeout = 5000) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
try {
const result = fn();
resolve(result);
} catch (error) {
if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout after ${timeout}ms: ${error.message}`));
} else {
setTimeout(check, 100);
}
}
};
check();
});
}
};
Continuous Integration Setup
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run unit tests
run: npm test -- --coverage --watchAll=false
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
- name: Run security audit
run: npm audit --audit-level high
Final Thoughts: Tests as Documentation
That production bug at Microsoft taught me that tests aren't just about catching errors - they're living documentation of how your code should behave. Well-written tests tell a story: "Given this situation, when this happens, then expect this result."
Key insights from building bulletproof applications:
- Start with tests - TDD forces better design
- Test behavior, not implementation - Focus on what, not how
- Mock external dependencies - Keep tests fast and reliable
- Organize tests well - They're code too
- Measure what matters - Coverage is a tool, not a goal
Whether you're fixing a critical bug or building a new feature, tests give you the confidence to refactor, optimize, and ship with certainty. Master testing, and you'll never fear deploying on Friday again.
Writing this from The Station Coffee House in Beacon Hill, where I first learned that "works on my machine" isn't good enough. Every line of code I write now has a test, and every test tells a story. Share your testing victories and horror stories @maya_codes_pnw - let's build more reliable software together! ☕🧪
Add Comment
No comments yet. Be the first to comment!