m dataclasses import dataclass, field
@dataclass
class InventoryManager:
sku: str
stock: int = 0
reserved: int = field(default=0, init=False)
def reserve_stock(self, quantity: int) -> bool:
if quantity <= 0:
raise ValueError("Quantity must be positive")
if quantity > self.stock:
return False
self.stock -= quantity
self.reserved += quantity
return True
def release_reservation(self, quantity: int) -> bool:
if quantity > self.reserved:
return False
self.reserved -= quantity
self.stock += quantity
return True
#### 2. Basic Assertion Pattern
pytest allows direct use of the `assert` statement. The framework intercepts assertions to provide detailed failure context, including variable values and expression diffs.
```python
# tests/test_inventory_basic.py
from src.inventory import InventoryManager
def test_successful_reservation():
inventory = InventoryManager(sku="WIDGET-A", stock=50)
result = inventory.reserve_stock(quantity=10)
assert result is True
assert inventory.stock == 40
assert inventory.reserved == 10
def test_reservation_failure_on_insufficient_stock():
inventory = InventoryManager(sku="WIDGET-B", stock=5)
result = inventory.reserve_stock(quantity=10)
assert result is False
assert inventory.stock == 5
assert inventory.reserved == 0
Rationale:
Tests are structured as plain functions. No class inheritance is required. Assertions verify both return values and internal state changes. This approach minimizes setup code and maximizes readability.
3. Reusable Fixtures with Scope Management
Fixtures provide a mechanism for sharing test setup logic. Scopes control resource lifecycle, preventing unnecessary recreation of expensive objects.
# tests/conftest.py
import pytest
from src.inventory import InventoryManager
@pytest.fixture(scope="function")
def empty_inventory():
"""Provides a fresh inventory instance for each test."""
return InventoryManager(sku="FIXTURE-SKU", stock=0)
@pytest.fixture(scope="session")
def valid_skus():
"""Shared list of SKUs across the entire test session."""
return ["SKU-001", "SKU-002", "SKU-003"]
# tests/test_inventory_fixtures.py
def test_reservation_with_fixture(empty_inventory):
empty_inventory.stock = 100
success = empty_inventory.reserve_stock(25)
assert success is True
assert empty_inventory.stock == 75
Rationale:
conftest.py centralizes fixture definitions, making them available across multiple test files. The function scope ensures test isolation, while session scope optimizes performance for immutable shared data.
4. Parameterized Testing
Parameterization allows a single test function to execute against multiple input combinations, reducing code duplication and ensuring comprehensive boundary coverage.
# tests/test_inventory_parametrize.py
import pytest
from src.inventory import InventoryManager
@pytest.fixture
def stocked_inventory():
return InventoryManager(sku="PARAM-SKU", stock=100)
@pytest.mark.parametrize("quantity,expected_success,expected_stock", [
(10, True, 90),
(50, True, 50),
(100, True, 0),
(101, False, 100),
(0, None, 100), # Edge case: zero quantity
])
def test_parametrized_reservations(stocked_inventory, quantity, expected_success, expected_stock):
if quantity == 0:
with pytest.raises(ValueError, match="Quantity must be positive"):
stocked_inventory.reserve_stock(quantity)
else:
result = stocked_inventory.reserve_stock(quantity)
assert result is expected_success
assert stocked_inventory.stock == expected_stock
Rationale:
pytest.mark.parametrize generates distinct test cases for each tuple. This pattern efficiently validates boundary conditions and error handling without replicating test logic.
5. Asynchronous Testing Support
Modern Python applications frequently rely on async I/O. pytest integrates with async code via the pytest-asyncio plugin, enabling seamless testing of coroutines.
# src/async_processor.py
import asyncio
async def process_inventory_update(sku: str, delta: int) -> dict:
await asyncio.sleep(0.05) # Simulate I/O
return {"sku": sku, "updated": True, "delta": delta}
# tests/test_inventory_async.py
import pytest
from src.async_processor import process_inventory_update
@pytest.mark.asyncio
async def test_async_inventory_update():
result = await process_inventory_update(sku="ASYNC-SKU", delta=-5)
assert result["sku"] == "ASYNC-SKU"
assert result["updated"] is True
assert result["delta"] == -5
Rationale:
The @pytest.mark.asyncio decorator handles the event loop management. Tests can directly await coroutines, ensuring async code paths are validated with the same rigor as synchronous logic.
Pitfall Guide
Production testing requires discipline. The following pitfalls are common in pytest adoption and their corresponding mitigations.
| Pitfall | Explanation | Fix |
|---|
| Mutable Fixture State Leakage | Using session or module scope with mutable objects causes tests to interfere with each other. | Use function scope for mutable state. If broader scope is necessary, ensure fixtures return immutable copies or reset state in teardown. |
| Testing Implementation Details | Asserting on private methods or internal attributes breaks tests when refactoring, even if behavior is unchanged. | Test public APIs and observable behavior. Use # noqa or explicit contracts if internal state verification is unavoidable. |
| Over-Reliance on Mocking | Excessive mocking creates tests that validate mocks rather than system behavior, leading to false confidence. | Mock only external dependencies (network, DB, file system). Prefer integration tests for internal logic. Use pytest-mock sparingly. |
| Slow Test Suites | Tests that perform real I/O or heavy computation delay feedback loops, discouraging frequent execution. | Use pytest-xdist for parallel execution. Mock slow dependencies. Categorize tests with markers (@pytest.mark.slow) to exclude them from rapid feedback cycles. |
| Implicit Fixture Dependencies | Tests that rely on fixtures without explicit injection become fragile and harder to understand. | Always declare fixture dependencies as function arguments. Avoid global state or implicit setup hooks. |
| Ignoring Assertion Context | Bare assertions like assert result provide poor failure messages when they fail. | Use descriptive variable names or add context: assert result == expected, f"Expected {expected}, got {result}". Leverage pytest's automatic introspection by keeping expressions simple. |
| Inconsistent Naming Conventions | Non-standard file or function names prevent pytest from auto-discovering tests. | Follow conventions: files named test_*.py or *_test.py, functions named test_*. Configure pytest.ini if deviations are necessary. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Utility Library | Simple functions + assert + parametrize | Low overhead, fast feedback, covers logic thoroughly | Minimal |
| Microservice with Database | Fixtures + testcontainers + pytest-asyncio | Realistic isolation, validates integration points | Moderate infra cost |
| ML Pipeline | parametrize + Mock data + pytest-cov | Reproducibility, handles large data variations efficiently | Low dev cost |
| High-Throughput API | Async tests + pytest-xdist + Markers | Validates concurrency, optimizes CI runtime | Low dev, high reliability |
| Legacy Codebase | pytest + pytest-mock + Incremental coverage | Non-intrusive adoption, allows gradual improvement | Moderate refactoring |
Configuration Template
Use this template to standardize pytest configuration across projects.
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests requiring external services",
"unit: marks fast, isolated unit tests"
]
addopts = [
"-v",
"--tb=short",
"--strict-markers",
"--import-mode=importlib"
]
# Coverage configuration
[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]
[tool.coverage.report]
fail_under = 80
show_missing = true
Quick Start Guide
-
Initialize Environment:
Run pip install pytest pytest-asyncio in your project root.
-
Create Test Directory:
Create a tests/ directory and add an empty __init__.py file.
-
Write First Test:
Create tests/test_example.py with a simple assertion:
def test_truth():
assert True
-
Execute Suite:
Run pytest from the command line. Verify output shows the test passing.
-
Validate Configuration:
Add pyproject.toml with the configuration template. Run pytest --collect-only to ensure discovery works correctly.