TL;DR
- Unit testing: Testing individual functions/methods in isolation
- Why it matters: Catches bugs early, enables safe refactoring, documents code behavior
- Key principle: Each test verifies ONE thing works correctly
- Popular frameworks: Jest (JavaScript), pytest (Python), JUnit (Java)
- Best practice: Write tests before fixing bugs to prevent regression
- ROI: Bugs caught at unit level cost 10-100x less to fix than in production
Reading time: 10 minutes
Unit testing is the foundation of software quality. It tests individual pieces of code in isolation, catching bugs before they spread to the rest of the system.
What is Unit Testing?
Unit testing tests the smallest testable parts of your code — functions and methods. Each unit test verifies that a specific piece of code produces the expected output for given inputs.
Integration Test: Login → API → Database → Response
(Many components working together)
Unit Test: validateEmail("test@example.com") → true
(Single function, isolated)
Unit tests run fast because they don’t involve databases, networks, or external services.
Why Unit Testing Matters
1. Catch Bugs Early
Bugs found during unit testing cost far less to fix:
| Stage Found | Relative Cost |
|---|---|
| Unit testing | 1x |
| Integration testing | 10x |
| System testing | 40x |
| Production | 100x+ |
Finding a bug in a unit test takes minutes. Finding it in production takes days.
2. Enable Safe Refactoring
With unit tests, you can refactor confidently:
// Original function
function calculatePrice(price, quantity) {
return price * quantity;
}
// Tests protect the refactoring
test('calculates total price', () => {
expect(calculatePrice(10, 3)).toBe(30);
});
// Safe to refactor - tests will catch mistakes
function calculatePrice(price, quantity, discount = 0) {
return (price * quantity) * (1 - discount);
}
Tests verify the function still works after changes.
3. Document Code Behavior
Tests show exactly how code should be used:
// Tests document expected behavior
test('returns empty array for null input', () => {
expect(filterUsers(null)).toEqual([]);
});
test('filters users by active status', () => {
const users = [
{ name: 'John', active: true },
{ name: 'Jane', active: false }
];
expect(filterUsers(users)).toEqual([{ name: 'John', active: true }]);
});
New developers understand behavior by reading tests.
4. Speed Up Development
Counterintuitively, writing tests speeds up development:
Without tests: Code → Manual test → Bug → Debug → Fix → Manual test
With tests: Code → Run tests → Bug → Fix → Run tests (seconds)
Automated tests give instant feedback.
Unit Test Structure
The AAA Pattern
Every unit test follows three steps:
test('adds two numbers correctly', () => {
// Arrange: Set up test data
const a = 5;
const b = 3;
// Act: Call the function
const result = add(a, b);
// Assert: Verify the result
expect(result).toBe(8);
});
This pattern makes tests readable and maintainable.
What to Test
- Happy path: Normal expected behavior
- Edge cases: Boundary conditions
- Error handling: Invalid inputs
describe('divide function', () => {
// Happy path
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
// Edge case
test('handles decimal results', () => {
expect(divide(10, 3)).toBeCloseTo(3.33, 2);
});
// Error handling
test('throws error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Writing Your First Unit Test
JavaScript (Jest)
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
// math.test.js
const { add, multiply } = require('./math');
describe('Math functions', () => {
test('adds positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('multiplies numbers', () => {
expect(multiply(4, 5)).toBe(20);
});
test('multiplies by zero', () => {
expect(multiply(4, 0)).toBe(0);
});
});
Run with npm test.
Python (pytest)
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_calculator.py
import pytest
from calculator import add, divide
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, -3) == -5
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
Run with pytest.
Java (JUnit)
// Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc = new Calculator();
@Test
void addPositiveNumbers() {
assertEquals(5, calc.add(2, 3));
}
@Test
void addNegativeNumbers() {
assertEquals(-5, calc.add(-2, -3));
}
@Test
void divideNumbers() {
assertEquals(5, calc.divide(10, 2));
}
@Test
void divideByZeroThrows() {
assertThrows(IllegalArgumentException.class,
() -> calc.divide(10, 0));
}
}
Mocking Dependencies
Unit tests should be isolated. Mock external dependencies:
// userService.js
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// userService.test.js
jest.mock('node-fetch');
test('fetches user by id', async () => {
// Mock the fetch response
fetch.mockResolvedValue({
json: () => Promise.resolve({ id: 1, name: 'John' })
});
const user = await getUser(1);
expect(user.name).toBe('John');
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
Mocking ensures tests run fast and don’t depend on external services.
Test Coverage
Understanding Coverage
Coverage measures how much code tests exercise:
Line coverage: What % of lines executed
Branch coverage: What % of if/else branches tested
Function coverage: What % of functions called
Practical Coverage Goals
| Code Type | Target Coverage |
|---|---|
| Business logic | 80-90% |
| Utility functions | 90%+ |
| UI components | 60-70% |
| Generated code | 0% (skip) |
Coverage is a guide, not a goal. High coverage doesn’t mean good tests.
// 100% coverage but useless test
test('covers the function', () => {
const result = complexCalculation(1, 2, 3);
expect(result).toBeDefined(); // Weak assertion
});
// Lower coverage but valuable test
test('calculates discount correctly', () => {
expect(calculateDiscount(100, 0.1)).toBe(90);
expect(calculateDiscount(100, 0.5)).toBe(50);
});
Test behavior, not coverage numbers.
Best Practices
1. Test One Thing
Each test should verify one behavior:
// Bad: Testing multiple things
test('user validation', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('')).toBe(false);
expect(validatePassword('abc')).toBe(false);
expect(validatePassword('abcd1234')).toBe(true);
});
// Good: One test per behavior
test('validates correct email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
test('rejects empty email', () => {
expect(validateEmail('')).toBe(false);
});
2. Use Descriptive Names
Test names should describe expected behavior:
// Bad names
test('test1', () => { ... });
test('validateEmail', () => { ... });
// Good names
test('rejects email without @ symbol', () => { ... });
test('accepts valid email with subdomain', () => { ... });
3. Keep Tests Independent
Tests should not depend on each other:
// Bad: Tests depend on shared state
let counter = 0;
test('increments counter', () => {
counter++;
expect(counter).toBe(1);
});
test('counter is one', () => {
expect(counter).toBe(1); // Fails if first test doesn't run
});
// Good: Each test is independent
test('increments counter', () => {
const counter = new Counter();
counter.increment();
expect(counter.value).toBe(1);
});
4. Write Tests First for Bug Fixes
When fixing bugs, write a test that reproduces it:
// Bug: calculateTax returns NaN for negative values
// Step 1: Write failing test
test('handles negative values', () => {
expect(calculateTax(-100)).toBe(0);
});
// Step 2: Fix the bug
function calculateTax(amount) {
if (amount < 0) return 0;
return amount * 0.1;
}
// Step 3: Test passes, bug won't return
Unit Testing vs Other Testing
| Type | What it Tests | Speed | Isolation |
|---|---|---|---|
| Unit | Single function | Fastest | Complete |
| Integration | Component interactions | Medium | Partial |
| E2E | Full user flows | Slowest | None |
Unit tests form the base of the testing pyramid:
/\
/ \ E2E (few)
/----\
/ \ Integration (some)
/--------\
/ \ Unit (many)
/____________\
More unit tests, fewer integration tests, fewest E2E tests.
FAQ
What is unit testing?
Unit testing tests individual functions or methods in complete isolation from the rest of the system. Each test focuses on one small piece of code — typically a single function — and verifies it produces the correct output for given inputs. The key characteristic is isolation: unit tests don’t involve databases, APIs, or other external dependencies. This isolation makes them fast (milliseconds) and reliable.
Why is unit testing important?
Unit testing catches bugs at the earliest and cheapest stage of development. A bug found during unit testing costs roughly 10-100x less to fix than one found in production. Beyond bug detection, unit tests enable confident refactoring (change code knowing tests will catch mistakes), serve as living documentation (showing exactly how code should behave), and speed up development through instant feedback loops.
What makes a good unit test?
Good unit tests follow the FIRST principles: Fast (run in milliseconds), Isolated (no external dependencies), Repeatable (same result every run), Self-validating (clear pass/fail), and Timely (written close to the code). They also follow the AAA pattern: Arrange test data, Act by calling the function, Assert the expected result. Each test should verify one specific behavior with a descriptive name.
How much unit test coverage do I need?
Aim for 70-80% code coverage on critical business logic, 90%+ on utility functions, and 60-70% on UI components. However, coverage is a guide, not a goal. 100% coverage doesn’t mean good tests — you can have complete coverage with weak assertions. Focus on testing meaningful behaviors rather than hitting coverage numbers. Skip testing generated code, getters/setters, and framework boilerplate.
See Also
- What is API Testing - API testing fundamentals
- Jest vs Mocha - JavaScript testing framework comparison
- TestNG vs JUnit - Java testing frameworks
- What is Regression Testing - Preventing bugs from returning
