Navigation

Programming

Unit Testing: From Debugging Nightmares to Confident Deployments

Master unit testing from a developer who built testing frameworks at Microsoft, covering Jest, TDD, mocking, coverage, and building bulletproof applications.

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

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:

  1. Start with tests - TDD forces better design
  2. Test behavior, not implementation - Focus on what, not how
  3. Mock external dependencies - Keep tests fast and reliable
  4. Organize tests well - They're code too
  5. 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! ☕🧪

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Programming