π§ͺ
Intermediate
6 min readTesting & Debugging
Unit testing, integration testing, and debugging techniques
Testing and debugging questions assess whether you write code defensively, can locate root causes quickly, and understand the trade-offs between different testing strategies. The goal is not 100% coverage β it is confidence that your code works as intended.
The Testing Pyramid
- Unit tests β test a single function or component in isolation; fast, many of them
- Integration tests β test how multiple units work together; slower, fewer
- End-to-end (E2E) tests β test the full user flow in a real browser; slowest, fewest
Unit Testing with Vitest/Jest
import { describe, it, expect, vi } from 'vitest';
import { formatCurrency } from './formatCurrency';
describe('formatCurrency', () => {
it('formats positive numbers', () => {
expect(formatCurrency(1234.5)).toBe('$1,234.50');
});
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('throws on negative', () => {
expect(() => formatCurrency(-1)).toThrow('Amount must be positive');
});
});
// Mocking β replace external dependencies
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));
React Component Testing with Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits form with valid credentials', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'alice@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'secret123');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'secret123',
});
});
});
test('shows error when email is invalid', async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.type(screen.getByLabelText(/email/i), 'notanemail');
await userEvent.tab();
expect(screen.getByRole('alert')).toHaveTextContent(/valid email/i);
});
Debugging Strategies
// 1. Binary search the bug β comment out half the code until the error disappears
// 2. Read the error message carefully β the line number is usually right
// 3. Use the debugger (better than console.log)
function complexFn(data) {
debugger; // browser will pause here with full call stack and scope
return data.map(transform);
}
// 4. Isolate in a minimal reproduction
// Reproduce the bug with the fewest possible lines of code
// 5. Check assumptions β what do you think the variable contains?
console.assert(typeof userId === 'string', 'userId must be a string, got:', userId);
Browser DevTools β Key Features
- Sources panel: set breakpoints, step through code, inspect scope at any point
- Network panel: inspect request/response headers, payload, timing; reproduce with "Copy as fetch"
- Performance panel: record and analyse long tasks, layout thrash, paint timing
- Memory panel: take heap snapshots to find memory leaks
- React DevTools: inspect component tree, props, state, and render reasons
Test-Driven Development (TDD)
Red β Green β Refactor: write a failing test first, write the minimum code to make it pass, then refactor. TDD is most valuable for pure functions and business logic β less so for UI components where requirements change frequently.