Testing Fundamentals

Understanding why we test, types of testing, and building a testing mindset

Why Testing Matters

Software testing is the process of verifying that your application works as expected and continues to work as you make changes. It's not just about finding bugsβ€”it's about building confidence, enabling rapid development, and serving as living documentation for your codebase.

🎯 The Value of Testing:

Tests allow you to refactor confidently, catch bugs early, document behavior, and ship faster. The upfront time investment pays dividends throughout the application's lifetime.

Benefits of Automated Testing

πŸ›‘οΈ Confidence in Changes

Refactor or add features without fear of breaking existing functionality. Tests act as a safety net.

πŸ“š Living Documentation

Tests describe how your code should behave. They're always up-to-date documentation.

πŸ› Early Bug Detection

Catch issues before they reach production. Bugs found early are cheaper and easier to fix.

πŸš€ Faster Development

Automated tests run in seconds. Manual testing takes minutes or hours for each change.

πŸ”„ Better Design

Test-driven development (TDD) often leads to more modular, maintainable code.

πŸ‘₯ Team Collaboration

New team members can change code confidently. Tests prevent regressions when multiple people work on the same codebase.

Types of Testing

πŸ”¬ Unit Testing

Testing individual functions, methods, or components in isolation.

Characteristics:

  • β€’ Fast execution (milliseconds)
  • β€’ Test one thing at a time
  • β€’ Mock external dependencies
  • β€’ Easy to debug when they fail

πŸ”— Integration Testing

Testing how multiple units work together.

Characteristics:

  • β€’ Test interactions between modules
  • β€’ May involve database, APIs, file system
  • β€’ Slower than unit tests
  • β€’ Catch integration issues

🌐 End-to-End (E2E) Testing

Testing complete user workflows from start to finish.

Characteristics:

  • β€’ Test from user perspective
  • β€’ Run in real browser
  • β€’ Slowest but highest confidence
  • β€’ Test full application stack

πŸ“Έ Snapshot Testing

Capturing and comparing component output.

Characteristics:

  • β€’ Detect unexpected UI changes
  • β€’ Useful for component libraries
  • β€’ Quick to write
  • β€’ Review changes carefully

Your First Unit Test

Let's write a simple unit test using Jest:

// sum.js - Function to test
export function sum(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}
// sum.test.js - Test file
import { sum, subtract, multiply, divide } from './sum';

describe('Math operations', () => {
  // Test for sum function
  test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('adds negative numbers correctly', () => {
    expect(sum(-1, -2)).toBe(-3);
  });

  // Test for subtract function
  test('subtracts 5 - 3 to equal 2', () => {
    expect(subtract(5, 3)).toBe(2);
  });

  // Test for multiply function
  test('multiplies 3 * 4 to equal 12', () => {
    expect(multiply(3, 4)).toBe(12);
  });

  test('multiplying by zero returns zero', () => {
    expect(multiply(5, 0)).toBe(0);
  });

  // Test for divide function
  test('divides 10 / 2 to equal 5', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws error when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
  });
});

// Run tests with: npm test

🎯 Test Anatomy:

  • describe(): Groups related tests together
  • test() or it(): Individual test case
  • expect(): Assertion about what should happen
  • Matcher: toBe(), toEqual(), toThrow(), etc.

Testing React Components

// Button.jsx - Component to test
export function Button({ onClick, disabled, children }) {
  return (
    
  );
}
// Button.test.jsx - Component test
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button component', () => {
  test('renders with correct text', () => {
    render();
    
    const button = screen.getByText('Click me');
    expect(button).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render();
    
    const button = screen.getByText('Click me');
    fireEvent.click(button);
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('does not call onClick when disabled', () => {
    const handleClick = jest.fn();
    render(
      
    );
    
    const button = screen.getByText('Click me');
    fireEvent.click(button);
    
    expect(handleClick).not.toHaveBeenCalled();
  });

  test('has disabled attribute when disabled prop is true', () => {
    render();
    
    const button = screen.getByText('Click me');
    expect(button).toBeDisabled();
  });
});

Test-Driven Development (TDD)

The TDD Cycle: Red-Green-Refactor

πŸ”΄ Red

Write a failing test

β†’
🟒 Green

Make it pass

β†’
πŸ”΅ Refactor

Improve the code

↻ Repeat the cycle

πŸ’‘ TDD Benefits:

  • β€’ Forces you to think about requirements first
  • β€’ Ensures every line of code is tested
  • β€’ Leads to better API design
  • β€’ Prevents over-engineering

Common Testing Patterns

Arrange-Act-Assert (AAA)

test('user can add item to cart', () => {
  // Arrange: Set up test data
  const cart = new ShoppingCart();
  const item = { id: 1, name: 'Book', price: 20 };
  
  // Act: Perform the action
  cart.addItem(item);
  
  // Assert: Verify the result
  expect(cart.items).toHaveLength(1);
  expect(cart.total).toBe(20);
});

Setup and Teardown

describe('Database tests', () => {
  let db;
  
  // Run before each test
  beforeEach(async () => {
    db = await connectDatabase();
    await db.clear();
  });
  
  // Run after each test
  afterEach(async () => {
    await db.disconnect();
  });
  
  test('can save user', async () => {
    const user = { name: 'Alice', email: 'alice@example.com' };
    await db.users.save(user);
    
    const savedUser = await db.users.findOne({ email: 'alice@example.com' });
    expect(savedUser.name).toBe('Alice');
  });
});

Parameterized Tests

// Test multiple inputs efficiently
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 2, 4],
  [-1, 1, 0],
  [0, 0, 0],
])('sum(%i, %i) returns %i', (a, b, expected) => {
  expect(sum(a, b)).toBe(expected);
});

⚠️ Common Testing Mistakes

  • β€’ Testing implementation details instead of behavior
  • β€’ Too many mocks that make tests brittle
  • β€’ Not testing edge cases (null, empty, boundary values)
  • β€’ Tests that depend on each other (should be independent)
  • β€’ Slow tests that developers skip
  • β€’ Unclear test names that don't describe what's being tested

πŸ’‘ Key Takeaways

  • βœ“ Tests are an investment that pays off over time
  • βœ“ Start with unit tests for core business logic
  • βœ“ Test behavior, not implementation
  • βœ“ Write tests that are easy to understand and maintain
  • βœ“ Fast tests get run more often
  • βœ“ TDD can lead to better design but isn't mandatory

πŸ“š More Testing Topics

Explore all 6 testing topics to build a comprehensive understanding of software testing.

View All Topics