TechLead
πŸ§ͺ
Intermediate
6 min read

Testing & 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.

Continue Learning