Chapter 18 of 21
Regular Expressions (re)
Current Study Module
Testing with pytest & TDD
Data Science: NumPy & Pandas
Chapter 17 • Quality Assurance & TDD

Testing with pytest, Fixtures & Mocking

Untested code is broken code by design. Learn why pytest has completely superseded legacy unittest, how bytecode assertion rewriting produces crystal-clear failure diagnostics, how to inject state using modular fixtures, and how to isolate systems with mocks.

1. Why pytest Dominates Python Testing

Legacy unittest required verbose class hierarchies and clumsy assertions (like self.assertEqual(a, b)).pytest allows you to write standard, plain Python assert statements:

# test_pricing.py
def calculate_discount(price: float, discount_percent: float) -> float:
    if not (0 <= discount_percent <= 100):
        raise ValueError("Discount must be between 0 and 100")
    return round(price * (1 - discount_percent / 100), 2)

def test_standard_discount():
    # Plain assert statement with automatic rich diff reporting on failure!
    assert calculate_discount(100.0, 20.0) == 80.0

def test_invalid_discount_raises_error():
    import pytest
    with pytest.raises(ValueError, match="Discount must be between 0 and 100"):
        calculate_discount(50.0, 150.0)

2. Dependency Injection with Fixtures (@pytest.fixture)

Fixtures provide a clean, modular way to manage test setup and teardown (e.g. database connections, temporary files):

import pytest

@pytest.fixture
def mock_user_database():
    """Setup test database connection."""
    db = {"users": {"101": {"name": "Alice", "role": "admin"}}}
    yield db  # Test executes here!
    # Teardown logic executes here after test finishes!
    db.clear()

def test_user_retrieval(mock_user_database):
    # Pytest injects the fixture automatically by parameter name!
    user = mock_user_database["users"].get("101")
    assert user["name"] == "Alice"
    assert user["role"] == "admin"

3. Parameterized Matrix Testing (@pytest.mark.parametrize)

Instead of writing ten near-identical test functions for boundary conditions, use @pytest.mark.parametrize to execute the same test across multiple input-output tuples:

import pytest

def is_valid_username(username: str) -> bool:
    return len(username) >= 3 and username.isalnum()

@pytest.mark.parametrize("username,expected", [
    ("alice", True),
    ("user123", True),
    ("ab", False),         # Too short
    ("invalid@user", False), # Illegal character
    ("", False)             # Empty string
])
def test_username_validation(username, expected):
    assert is_valid_username(username) == expected

4. Mocking External APIs with unittest.mock

Tests should never hit live external payment gateways or third-party web services. Use unittest.mock.patch to isolate and simulate external responses:

from unittest.mock import patch

def get_current_bitcoin_price() -> float:
    import requests
    response = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json")
    return response.json()["bpi"]["USD"]["rate_float"]

# Test without sending actual internet network packets!
@patch("requests.get")
def test_bitcoin_price_fetch(mock_get):
    # Configure mock return object
    mock_get.return_value.json.return_value = {
        "bpi": {"USD": {"rate_float": 68450.25}}
    }

    price = get_current_bitcoin_price()
    assert price == 68450.25
    mock_get.assert_called_once()

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools