Introduction to Unit Testing with pytest

pytest replaced Python’s built-in unittest as the de facto standard for a reason: plain assert statements instead of a wall of self.assertEqual() calls, powerful fixtures, and genuinely readable failure output.

A Test Is Just a Function

from calculator import add, divide

def test_add():
    assert add(2, 3) == 5

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero_raises():
    import pytest
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)
pip install pytest
pytest                    # runs every test_*.py file it finds
pytest -v                 # verbose output, one line per test
pytest test_calculator.py::test_add   # run a single specific test

Fixtures: Reusable Setup

import pytest

@pytest.fixture
def sample_cart():
    cart = ShoppingCart()
    cart.add_item("Widget", 9.99, 2)
    return cart

def test_cart_total(sample_cart):
    assert sample_cart.total() == 19.98

def test_cart_item_count(sample_cart):
    assert len(sample_cart.items) == 1

Each test that requests sample_cart as a parameter gets a fresh instance — no shared state leaking between tests, and no repeated setup code copy-pasted across every function.

Parametrizing to Cover Multiple Cases

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add_various(a, b, expected):
    assert add(a, b) == expected

Measuring Coverage

pip install pytest-cov
pytest --cov=calculator --cov-report=term-missing

Coverage percentage is a useful signal, not a goal to game — 100% coverage with weak assertions catches far fewer bugs than 70% coverage of the code paths that actually matter.