Unit Testing in Python
Unit Testing in Python
Section titled “Unit Testing in Python”pytest is Python’s most popular testing framework — minimal boilerplate, powerful fixtures, and great plugin support.
pip install pytest pytest-cov pytest-mockpytest.ini (or pyproject.toml):
[pytest]testpaths = testspython_files = test_*.py *_test.pypython_functions = test_*Writing Tests
Section titled “Writing Tests”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 / bfrom src.calculator import add, divideimport 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)Parametrize
Section titled “Parametrize”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) == expectedFixtures
Section titled “Fixtures”Reusable setup that’s injected into tests:
import pytestfrom src.user_service import UserServicefrom src.repositories import UserRepository
@pytest.fixturedef user_repo(): return UserRepository(db_url="sqlite:///:memory:")
@pytest.fixturedef 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"Mocking with pytest-mock
Section titled “Mocking with pytest-mock”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!", )Using unittest.mock Directly
Section titled “Using unittest.mock Directly”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" )Fixtures Scope
Section titled “Fixtures Scope”Control how often a fixture is created:
@pytest.fixture(scope="session") # created once per test sessiondef 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 testTemporary Files and Directories
Section titled “Temporary Files and Directories”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.
Markers
Section titled “Markers”@pytest.mark.slowdef 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(): ...pytest -m "not slow" # skip tests marked slowpytest -m slow # run only slow testsRunning Tests
Section titled “Running Tests”pytest # all testspytest tests/test_user.py # specific filepytest -k "test_create" # tests matching patternpytest -v # verbosepytest -x # stop on first failurepytest --cov=src --cov-report=html # with coveragepytest -n auto # parallel (install pytest-xdist)