| name | python-testing |
| description | Python testing patterns using pytest — fixtures, mocking, parametrization, async tests, and TDD methodology. Always activate when the user is writing, fixing, or designing tests; setting up a test suite; asking about coverage; or working with pytest configuration. Trigger on phrases like "write a test", "add tests for", "how do I test", "mock this", "fixture for", "parametrize", "test coverage", "pytest setup", "TDD", "how do I assert", "test this function", or any request explicitly about testing Python code. Also activate proactively when the user is writing new Python code without any tests — TDD means tests come first. |
Python Testing Patterns
Comprehensive pytest patterns for Python applications — TDD, fixtures, mocking, parametrization, and async testing.
Workflow
When this skill activates:
- Identify the task — new test suite, specific test case, mocking problem, coverage gap, config setup, or TDD session.
- Apply TDD by default when writing new code — red (failing test) → green (minimal implementation) → refactor.
- Navigate to the right section below. For quick lookups, use the Quick Reference table.
- Read
references/patterns.md for: markers and test selection, async testing, exception testing, file I/O testing, test organization, API/DB patterns, and full pytest config.
- Flag anti-patterns proactively if spotted in user's existing tests.
Core Philosophy
TDD Cycle
def test_parse_price():
assert parse_price("$12.99") == 12.99
def parse_price(s: str) -> float:
return float(s.lstrip("$"))
def parse_price(s: str) -> float:
return float(s.replace("$", "").replace(",", "").strip())
Coverage
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
Target 80%+ overall; 100% on critical paths (auth, payment, data validation). Coverage measures lines executed — it does not prove correctness.
Fixtures
Basic and Setup/Teardown
@pytest.fixture
def sample_user():
return {"name": "Alice", "age": 30}
@pytest.fixture
def database():
db = Database(":memory:")
db.create_tables()
yield db
db.close()
Fixture Scopes
@pytest.fixture
def temp_config(): ...
@pytest.fixture(scope="module")
def module_db(): ...
@pytest.fixture(scope="session")
def shared_resource(): ...
Use scope="session" for expensive resources (DB connections, ML models). Use scope="function" for anything that mutates state.
conftest.py — Shared Fixtures
@pytest.fixture
def client():
app = create_app(testing=True)
with app.test_client() as c:
yield c
@pytest.fixture
def auth_headers(client):
resp = client.post("/api/login", json={"username": "test", "password": "test"})
return {"Authorization": f"Bearer {resp.json['token']}"}
Autouse and Parametrized Fixtures
@pytest.fixture(autouse=True)
def reset_state():
"""Runs automatically before every test in scope."""
Config.reset()
yield
Config.cleanup()
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
"""Test runs once per param value."""
return make_db(request.param)
Parametrization
@pytest.mark.parametrize("value,expected", [
("hello", "HELLO"),
("world", "WORLD"),
])
def test_upper(value, expected):
assert value.upper() == expected
@pytest.mark.parametrize("addr,valid", [
("user@example.com", True),
("not-an-email", False),
("@no-local.com", False),
], ids=["valid", "missing-at", "missing-local"])
def test_email(addr, valid):
assert is_valid_email(addr) is valid
Prefer ids= whenever the parameter values aren't self-explanatory in test output.
Mocking
Use pytest-mock's mocker fixture instead of raw @patch decorators for cleaner syntax and automatic cleanup:
def test_api_call(mocker):
mock = mocker.patch("mypackage.requests.get")
mock.return_value.json.return_value = {"status": "ok"}
result = fetch_data()
mock.assert_called_once_with("https://api.example.com/data")
assert result["status"] == "ok"
from unittest.mock import patch, Mock, MagicMock, mock_open
@patch("mypackage.external_api_call")
def test_with_patch(api_mock):
api_mock.return_value = {"status": "ok"}
result = my_function()
api_mock.assert_called_once()
assert result["status"] == "ok"
⚠️ Decorator Stacking Order — Common Footgun
When stacking multiple @patch decorators, mocks are injected bottom-up (innermost decorator → first argument):
@patch("mypackage.ServiceB")
@patch("mypackage.ServiceA")
def test_stacked(mock_a, mock_b):
...
This trips up nearly everyone. Prefer mocker.patch() which avoids argument ordering entirely.
Side Effects, Exceptions, Properties
mock.side_effect = ConnectionError("timeout")
mock.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
from unittest.mock import PropertyMock
type(mock_obj).debug = PropertyMock(return_value=True)
mocker.patch("builtins.open", mock_open(read_data="file content"))
Always Use autospec
mock_db = mocker.patch("mypackage.DBConnection", autospec=True)
mock_db.return_value.query("SELECT 1")
Mock vs MagicMock
Mock — basic mock; magic methods (__len__, __iter__, __enter__) not pre-configured
MagicMock — magic methods pre-configured; use for context managers, iterables, and containers
mocker.patch() returns MagicMock by default
Assertions
assert result == expected
assert result is None
assert isinstance(result, list)
assert "error" in message
assert 0 < score <= 100
with pytest.raises(ValueError, match="invalid input"):
validate("")
with pytest.raises(CustomError) as exc_info:
risky_call()
assert exc_info.value.code == 400
Quick Reference
| Pattern | Use it for |
|---|
@pytest.fixture | Reusable setup/teardown |
yield in fixture | Teardown after yield runs even on failure |
scope="session" | Expensive shared resources |
conftest.py | Cross-module shared fixtures |
@pytest.mark.parametrize | Multiple inputs, one test function |
ids=["name1", ...] | Readable parametrize output |
mocker.patch() | Mock with auto-cleanup (pytest-mock) |
autospec=True | Catch wrong method signatures in mocks |
MagicMock | Context managers, iterables |
mock.side_effect | Exceptions or sequential return values |
pytest.raises(Exc, match=r"...") | Assert exception type and message |
tmp_path | Built-in temp directory (pathlib), auto-cleaned |
--lf | Re-run only last failed tests |
-x | Stop at first failure |
-k "pattern" | Run tests matching name pattern |
Best Practices
DO:
- Write tests before code (TDD: red → green → refactor)
- Test one behavior per test function
- Use descriptive names:
test_login_with_expired_token_returns_401
- Use
autospec=True on all mocks
- Use
tmp_path for file I/O (never raw tempfile in tests)
- Test edge cases:
None, empty string, empty list, boundary values
- Keep tests independent — no shared mutable state between tests
DON'T:
- Test implementation details — test observable behavior
- Use
@patch stacks without carefully checking argument order
- Put
--disable-warnings in addopts — it hides real problems
- Use
tmpdir — it's deprecated; use tmp_path instead
- Catch exceptions inside tests — use
pytest.raises instead
- Write tests that pass only in a specific order
For extended patterns (markers, async testing, API/DB testing, test organization, full pytest config), see references/patterns.md.