pytest Fundamentals
What You'll Learn
How to install pytest, write tests that actually catch bugs, run them, and interpret the results.
Installing pytest
pip install pytest
pip install pytest-cov # for coverage reports
Your First Test
Create a file named test_math.py (test files must start with test_):
# math_utils.py — the code you want to test
def add(a: int, b: int) -> int:
return a + b
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_math.py — tests for math_utils.py
from math_utils import add, divide
import pytest
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -2) == -3
def test_add_zero():
assert add(0, 0) == 0
def test_divide_normal():
assert divide(10, 2) == 5.0
def test_divide_by_zero_raises():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
Run tests:
pytest # all tests in current directory
pytest test_math.py # specific file
pytest -v # verbose output
pytest -v -k "divide" # only tests with "divide" in the name
Output:
test_math.py::test_add_positive_numbers PASSED
test_math.py::test_add_negative_numbers PASSED
test_math.py::test_add_zero PASSED
test_math.py::test_divide_normal PASSED
test_math.py::test_divide_by_zero_raises PASSED
5 passed in 0.12s
Test File Organization
project/
├── src/
│ └── myapp/
│ ├── utils.py
│ └── processor.py
└── tests/
├── conftest.py ← shared fixtures
├── test_utils.py
└── test_processor.py
pytest finds tests by searching for:
- Files matching
test_*.pyor*_test.py - Functions starting with
test_ - Classes starting with
Test
Assertions — What You Can Test
# Equality
assert result == 42
assert name == "Alice"
assert items == [1, 2, 3]
# Truth/falsy
assert is_active
assert not is_banned
# Comparison
assert score >= 90
assert price < 100.0
# Membership
assert "admin" in user["roles"]
assert "error" not in log_output
# Types
assert isinstance(result, dict)
assert isinstance(items, list)
# None
assert user is None
assert config is not None
# Exceptions
with pytest.raises(ValueError):
risky_function()
with pytest.raises(ValueError, match="specific message"):
risky_function()
# Approximate floating point
assert abs(result - 3.14159) < 0.001
import pytest
assert result == pytest.approx(3.14159, rel=1e-3)
Parametrize — Test Many Inputs at Once
import pytest
from math_utils import add
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
(100, -50, 50),
(1.5, 1.5, 3.0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
This runs 5 separate tests — each shown individually if one fails.
Testing Classes
from myapp.models import ShoppingCart, Product
class TestShoppingCart:
def test_empty_cart_total(self):
cart = ShoppingCart()
assert cart.total() == 0.0
def test_add_item(self):
cart = ShoppingCart()
cart.add(Product("apple", price=1.50), quantity=3)
assert cart.total() == 4.50
def test_remove_item(self):
cart = ShoppingCart()
cart.add(Product("apple", price=1.50), quantity=2)
cart.remove("apple")
assert cart.total() == 0.0
def test_empty_cart_raises_on_checkout(self):
cart = ShoppingCart()
with pytest.raises(ValueError, match="cannot checkout empty cart"):
cart.checkout()
Code Coverage
# Run tests with coverage
pytest --cov=src --cov-report=term-missing
# Generate HTML report
pytest --cov=src --cov-report=html
open htmlcov/index.html
Coverage output:
Name Stmts Miss Cover Missing
---------------------------------------------------
src/myapp/utils.py 25 3 88% 45-47
src/myapp/processor.py 40 0 100%
---------------------------------------------------
TOTAL 65 3 95%
Aim for 80–90% coverage. 100% is rarely worth the effort.
Running Only Failed Tests
pytest --lf # run only tests that failed last time
pytest --ff # run failed tests first, then rest
pytest -x # stop after first failure
pytest --tb=short # shorter traceback format
Common Mistakes
| Mistake | Fix |
|---|---|
Test file not named test_*.py | Must start with test_ |
Test function not named test_* | Must start with test_ |
| Testing implementation details | Test behavior and outputs |
| One mega-test that tests everything | One assertion focus per test |
| No test for error cases | Always test unhappy paths |
Quick Reference
# Install
pip install pytest pytest-cov
# Run
pytest # all tests
pytest -v # verbose
pytest test_file.py # specific file
pytest -k "keyword" # filter by name
pytest --lf # failed only
pytest -x # stop on first fail
pytest --cov=src --cov-report=term-missing
# Assert
assert result == expected
assert pytest.approx(3.14) == result
with pytest.raises(ValueError):
fn()
# Parametrize
@pytest.mark.parametrize("input, expected", [(1, 2), (3, 4)])
def test_fn(input, expected):
assert fn(input) == expected