Module 10 ยท Lesson 33

Testing with pytest

๐Ÿ Pythonโฑ 15 min read๐Ÿ“– Professional

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 -v

Testing 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 code

Test 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

Practice Exercises

  1. Write a full test suite for a Calculator class with add, subtract, multiply, divide methods. Cover edge cases.
  2. Write parametrized tests for a validate_email function โ€” include valid and invalid emails.
  3. Write a test that mocks the requests.get function and tests a function that fetches data from an API.
  4. Set up pytest with coverage reporting. Aim for 100% coverage on a small utility module.
โ† Type Hints