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
Write a failing test
Make it pass
Improve the code
π‘ 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