testing-python

SKILL.md

Python Testing Patterns

Implement comprehensive Python testing strategies with pytest.

Use When

Writing Python unit/integration/functional/perf tests.

Core Rules

  • AAA: Arrange / Act / Assert
  • Isolated, deterministic tests
  • Descriptive names (why, not just what)
  • Fixtures for setup/teardown
  • Parametrize to reduce duplication
  • Mock externals
  • Test edges/errors
  • Coverage: meaningful > %

For TDD workflow (Red-Green-Refactor cycle, naming conventions, quality principles) refer to @tdd-best-practices.md

Patterns

1) Class method

# calculator.py
class Calculator:
    def add(self, a: float, b: float) -> float:
        return a + b
# tests/test_calculator.py
from calculator import Calculator

class TestAdd:
    def test_commutative(self):
        c = Calculator()
        assert c.add(2,3) == c.add(3,2)

    def test_identity(self):
        c = Calculator()
        assert c.add(8,0) == c.add(0,8) == 8

    def test_inverse(self):
        c = Calculator()
        assert c.add(8,-8) == c.add(-8,8) == 0

2) Function

# calculator.py
def add(a: float, b: float) -> float:
    return a + b
# tests/test_calculator.py
from calculator import add

class TestAdd:
    def test_commutative(self):
        assert add(2,3) == add(3,2)

3) Fixtures (setup/teardown via yield)

# database.py
class Database:
    def __init__(self, cs: str):
        self.cs = cs; self.connected = False
    def connect(self): self.connected = True
    def disconnect(self): self.connected = False
    def query(self, sql: str):
        if not self.connected: raise RuntimeError("Not connected")
        return [{"id": 1, "name": "Test"}]
# tests/test_database.py
import pytest
from typing import Generator
from database import Database

@pytest.fixture
def db() -> Generator[Database, None, None]:
    database = Database("sqlite:///:memory:")
    database.connect()
    yield database
    database.disconnect()

class TestQuery:
    def test_returns_one_record(self, db: Database):
        # Arrange
        expected = [{"id": 1, "name": "Test"}]
        # Act
        results = db.query("SELECT * FROM users")
        # Assert
        assert results == expected

4) Parametrize

import pytest
from pytest import param

@pytest.mark.parametrize("email,expected", [
    param("user@example.com", True, id="valid"),
    param("invalid.email", False, id="missing @"),
])
def test_is_valid_email(email, expected):
    assert is_valid_email(email) == expected

5) Mocking (pytest-mock)

# api_client.py
import requests
class APIClient:
    def get_data(self, url: str):
        return requests.get(url).json()
# tests/test_api_client.py
from api_client import APIClient

def test_returns_json(mocker):
    mock_resp = mocker.Mock()
    mock_resp.json.return_value = {"key":"value"}
    mocker.patch("requests.get", return_value=mock_resp)

    assert APIClient().get_data("http://x") == {"key":"value"}

6) Exceptions

import pytest
from pytest import param
from contextlib import nullcontext as does_not_raise

@pytest.mark.parametrize("a,b,expect,ans", [
    param(30, 2.5, does_not_raise(), 12.0, id="ok"),
    param(10, 0, pytest.raises(ZeroDivisionError, match="zero"), None, id="zero"),
])
def test_divide(a,b,expect,ans):
    with expect:
        assert divide(a,b) == ans

For more information refer to @testing-exceptions.md

7) Async

import pytest

@pytest.mark.asyncio
async def test_do_something_returns_expected_result():
    res = await library.do_something()
    assert res == b"expected result"

For more information refer to @testing-async-code.md

8) tmp_path

def test_write_text_is_successful(tmp_path):
    p = tmp_path / "test.txt"
    p.write_text("Hello")
    assert p.read_text() == "Hello"

For more details refer to @temporary-files-and-dirs.md

9) conftest.py

Use conftest.py to share fixtures; pytest auto-discovers it.

For more details refer to @custom-fixtures-and-conftest.md

10) Fixture scopes

Control how often a fixture is created/destroyed with the scope parameter:

@pytest.fixture(scope="function")  # default: new instance per test
def func_scoped(): ...

@pytest.fixture(scope="class")     # one instance per test class
def class_scoped(): ...

@pytest.fixture(scope="module")    # one instance per test file
def module_scoped(): ...

@pytest.fixture(scope="session")   # one instance for the entire test run
def session_scoped(): ...

Use broader scopes for expensive setup (e.g. DB connections, server processes).

11) monkeypatch

Use the built-in monkeypatch fixture to temporarily set env vars, replace attributes, or modify dicts:

def test_uses_custom_env_var(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key-123")
    assert get_api_key() == "test-key-123"

def test_patches_attribute(monkeypatch):
    monkeypatch.setattr("mymodule.DEFAULT_TIMEOUT", 0)
    assert fetch_with_timeout() == "ok"

All changes are automatically undone after the test.

Organization

tests/
  conftest.py
  test_module1/
    test_models.py
  test_utils.py

Naming

  • Class: Test<Thing>
  • Test names explain behavior/intent
  • Examples:
    • test_is_not_adult_if_age_less_than_18
    • test_fails_if_account_invalid

Coverage

uv run pytest --cov-report=term-missing tests/

For coverage thresholds, critical path rules, and avoiding coverage theater refer to @tdd-best-practices.md

Weekly Installs
2
GitHub Stars
1
First Seen
10 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2