Skip to content

Unit Testing in Python

pytest is Python’s most popular testing framework — minimal boilerplate, powerful fixtures, and great plugin support.

Terminal window
pip install pytest pytest-cov pytest-mock

pytest.ini (or pyproject.toml):

[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_functions = test_*
src/calculator.py
def add(a: float, b: float) -> float:
return a + b
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
tests/test_calculator.py
from src.calculator import add, divide
import pytest
def test_add_returns_sum():
assert add(2, 3) == 5
def test_add_with_negatives():
assert add(-1, 1) == 0
def test_divide_returns_quotient():
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 the same test with multiple inputs:

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

Reusable setup that’s injected into tests:

import pytest
from src.user_service import UserService
from src.repositories import UserRepository
@pytest.fixture
def user_repo():
return UserRepository(db_url="sqlite:///:memory:")
@pytest.fixture
def user_service(user_repo):
return UserService(user_repo)
def test_create_user(user_service):
user = user_service.create(name="Alice", email="alice@example.com")
assert user.id is not None
assert user.name == "Alice"
def test_get_user(user_service):
created = user_service.create(name="Bob", email="bob@example.com")
fetched = user_service.get(created.id)
assert fetched.email == "bob@example.com"
def test_send_welcome_email(user_service, mocker):
mock_send = mocker.patch("src.email_service.send_email")
user_service.register(name="Alice", email="alice@example.com")
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome!",
)
from unittest.mock import Mock, patch, MagicMock
def test_fetch_user_from_api():
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_response.status_code = 200
with patch("requests.get", return_value=mock_response) as mock_get:
result = fetch_user_from_api(user_id=1)
assert result["name"] == "Alice"
mock_get.assert_called_once_with(
"https://api.example.com/users/1"
)

Control how often a fixture is created:

@pytest.fixture(scope="session") # created once per test session
def db_connection():
conn = create_db_connection()
yield conn
conn.close()
@pytest.fixture(scope="module") # once per test module
@pytest.fixture(scope="class") # once per test class
@pytest.fixture(scope="function") # default: once per test
def test_write_report(tmp_path):
report_file = tmp_path / "report.txt"
write_report(output=report_file, data={"total": 42})
assert report_file.read_text() == "Total: 42"

tmp_path is a built-in pytest fixture that provides an isolated temporary directory.

@pytest.mark.slow
def test_large_dataset_processing():
...
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
...
@pytest.mark.xfail(reason="Known bug — JIRA-456")
def test_known_failing():
...
Terminal window
pytest -m "not slow" # skip tests marked slow
pytest -m slow # run only slow tests
Terminal window
pytest # all tests
pytest tests/test_user.py # specific file
pytest -k "test_create" # tests matching pattern
pytest -v # verbose
pytest -x # stop on first failure
pytest --cov=src --cov-report=html # with coverage
pytest -n auto # parallel (install pytest-xdist)