Why Test?
Tests are your safety net. They verify code works as expected, catch regressions when you change things, serve as documentation, and let you refactor confidently. Professional Python code always has tests.
# Install pytest
# pip install pytest pytest-cov
# test_math.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 functions start with test_
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_add_floats():
result = add(0.1, 0.2)
assert abs(result - 0.3) < 1e-9 # Float comparison!
def test_divide():
assert divide(10, 2) == 5.0
assert divide(7, 2) == 3.5
# Run: pytest test_math.py
# Run all tests: pytest
# Verbose: pytest -vTesting Exceptions
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
# Check exception message
def test_divide_by_zero_message():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
# Check exception type and value
def test_index_error():
with pytest.raises(IndexError) as exc_info:
[][0] # Index empty list
assert "list index out of range" in str(exc_info.value)Fixtures
import pytest
# Fixtures provide test data/setup
@pytest.fixture
def sample_user():
return {"name": "Alice", "age": 30, "email": "alice@example.com"}
@pytest.fixture
def empty_db():
db = Database(":memory:")
db.create_tables()
yield db # yield instead of return โ teardown code runs after
db.close() # Cleanup runs here
def test_user_name(sample_user):
assert sample_user["name"] == "Alice"
def test_user_is_adult(sample_user):
assert sample_user["age"] >= 18
def test_create_user(empty_db):
empty_db.insert("users", {"name": "Bob"})
assert empty_db.count("users") == 1
# Fixture scope: function (default), class, module, session
@pytest.fixture(scope="module")
def expensive_setup():
# Created once per module, shared across tests
return setup_expensive_resource()Parameterized Tests and Mocking
import pytest
from unittest.mock import patch, MagicMock
# Parametrize โ test multiple inputs
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, -50, 50),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
# Runs 4 separate test cases!
# Mocking โ replace real dependencies with fakes
def get_user_from_api(user_id):
import requests
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
def test_get_user():
mock_response = MagicMock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
with patch("requests.get", return_value=mock_response):
user = get_user_from_api(1)
assert user["name"] == "Alice"
# Mock raises exception
def test_api_failure():
with patch("requests.get", side_effect=ConnectionError("Network down")):
with pytest.raises(ConnectionError):
get_user_from_api(1)Coverage
# pip install pytest-cov
# Run tests with coverage
pytest --cov=my_module --cov-report=html
# Output:
# Name Stmts Miss Cover
# my_module.py 20 3 85%
# Coverage shows untested lines
# Aim for 80%+ in production codeTest Organization
# Project structure
tests/
__init__.py
conftest.py # Shared fixtures (auto-loaded by pytest)
test_models.py
test_utils.py
test_api.py
integration/
test_database.py
# conftest.py โ shared fixtures
# @pytest.fixture
# def db():
# ...
# Mark tests
@pytest.mark.slow
def test_large_dataset():
...
# Run: pytest -m "not slow" (skip slow tests)Key Takeaways
- Name functions test_*: pytest auto-discovers them
- assert is all you need: pytest shows detailed failure messages
- Fixtures for setup/teardown: use yield for cleanup
- @pytest.mark.parametrize: test many cases without duplicating code
- Mock external dependencies: tests should be fast and not need network
Practice Exercises
- Write a full test suite for a
Calculatorclass with add, subtract, multiply, divide methods. Cover edge cases. - Write parametrized tests for a
validate_emailfunction โ include valid and invalid emails. - Write a test that mocks the
requests.getfunction and tests a function that fetches data from an API. - Set up pytest with coverage reporting. Aim for 100% coverage on a small utility module.