Skip to main content

pytest

pytest testing framework conventions and practices. Invoke whenever task involves any interaction with pytest — writing tests, configuring pytest, fixtures, parametrize, mocking, debugging test failures, or coverage.

Source facts

Repository
xobotyi/cc-foundry
Last source activity
October 28, 2026 at 20:49
Detected SKILL.md language
English
Stars
23
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
6 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
pytest
description
pytest testing framework conventions and practices. Invoke whenever task involves any interaction with pytest — writing tests, configuring pytest, fixtures, parametrize, mocking, debugging test failures, or coverage.
# pytest **Test behavior, not implementation. Tests are executable documentation — if the test name doesn't explain what the code does, rewrite it.** pytest is Python's standard testing framework. It uses plain `assert` statements, fixtures for setup/teardown, and a rich plugin ecosystem. All patterns target Python 3.14+. ## References - **Fixture patterns, scope, factories, teardown** — [`${CLAUDE_SKILL_DIR}/references/fixtures.md`]: Fixture lifecycle, yield fixtures, factory pattern, request object, parametrized fixtures - **Parametrize patterns, indirect, IDs** — [`${CLAUDE_SKILL_DIR}/references/parametrize.md`]: Multi-parameter examples, indirect fixtures, custom IDs, stacking decorators - **Monkeypatch patterns, scoped patches** — [`${CLAUDE_SKILL_DIR}/references/monkeypatch.md`]: API overview, attribute/env/dict patching, scoped monkeypatch, common recipes - **Plugin ecosystem and configuration** — [`${CLAUDE_SKILL_DIR}/references/plugins.md`]: pytest-asyncio, pytest-mock, pytest-xdist, pytest-cov configuration patterns ## Test Structure ### Discovery and Naming - **Files:** `test_*.py` or `*_test.py`. Prefer `test_<module>.py` matching source module. - **Functions:** `test_<behavior>` — describe the behavior, not the method: `test_returns_empty_list_when_no_matches` not `test_search`. - **Classes:** `TestClassName` groups related tests. No `__init__` method. Use classes when tests share setup; use bare functions for independent tests. - **conftest.py** is auto-discovered — no import needed. Place shared fixtures at the appropriate directory level. ### Arrange-Act-Assert Structure every test in three phases: ```python def test_user_creation_sets_defaults(): # Arrange data = {"name": "Alice", "email": "alice@example.com"} # Act user = User.from_dict(data) # Assert assert user.name == "Alice" assert user.is_active is True assert user.roles == [] ``` - **One act per test.** If you need multiple acts, write multiple tests. - **Comments optional** when phases are obvious. Add when the test is long enough that phases aren't immediately clear. ### Test Granularity - **One concept per test.** Multiple assertions are fine when they verify the same behavior. Separate tests when behaviors are independent. - **Fast by default.** Unit tests should run in milliseconds. Gate slow tests (network, DB) behind markers: `@pytest.mark.slow`. - **Isolation is mandatory.** Tests must not depend on execution order or shared mutable state. Each test sets up its own world. ## Fixtures ### Core Rules - **Fixtures over setup methods.** Fixtures are composable, scoped, and explicit. Never use `setUp`/`tearDown` from `unittest`. - **Explicit injection.** Request fixtures by name in test parameters. Every dependency is visible in the test signature. - **Smallest viable scope.** Default is `function` scope (fresh per test). Use broader scopes (`class`, `module`, `session`) only for expensive resources. - **`autouse=True` sparingly.** Only for setup that genuinely applies to every test in scope (e.g., database transaction rollback, temp directory cleanup). ### Yield Fixtures (Setup + Teardown) ```python @pytest.fixture def db_connection(): conn = create_connection() yield conn conn.close() @pytest.fixture def temp_config(tmp_path: Path): config_file = tmp_path / "config.toml" config_file.write_text('[app]\ndebug = true\n') yield config_file # cleanup automatic — tmp_path handles it ``` - **`yield`** separates setup from teardown. Code after `yield` runs even if the test fails. - **Prefer `yield`** over `addfinalizer` — clearer control flow. - **Teardown must not raise.** If cleanup can fail, wrap in `try`/`except` and log. ### Fixture Factories When tests need multiple instances with varying configuration: ```python @pytest.fixture def make_user(): def _make_user(name: str = "Alice", *, active: bool = True) -> User: return User(name=name, is_active=active) return _make_user def test_inactive_users_excluded(make_user): active = make_user("Alice", active=True) inactive = make_user("Bob", active=False) assert filter_active([active, inactive]) == [active] ``` ### Fixture Scope - **`function`** — Each test (default). Most fixtures — cheap setup, isolation. - **`class`** — All tests in a class. Shared expensive setup within a test class. - **`module`** — All tests in a file. Database connection per test file. - **`session`** — Entire test run. Server startup, heavy resource initialization. - **Session-scoped fixtures** must be in `conftest.py` at the root test directory. - **Don't mix scopes carelessly.** A function-scoped fixture cannot depend on a function-scoped fixture that modifies state from a broader scope. ### Built-in Fixtures - **`tmp_path`** — `Path` to a temporary directory unique to the test (function scope) - **`tmp_path_factory`** — Factory for creating temp directories (session scope) - **`capsys`** — Capture `sys.stdout`/`sys.stderr` writes - **`capfd`** — Capture file descriptor 1/2 output (catches C-level writes) - **`caplog`** — Capture `logging` output with access to records - **`monkeypatch`** — Dynamic attribute/env/dict patching with automatic restore - **`request`** — Fixture metadata: `.param`, `.node`, `.config`, `.fspath` - **`pytestconfig`** — Access to the pytest config object See `${CLAUDE_SKILL_DIR}/references/fixtures.md` for fixture lifecycle details, parametrized fixtures, and advanced patterns. ## Parametrize ### Basic Usage ```python @pytest.mark.parametrize("input_val, expected", [ ("hello", 5), ("", 0), (" spaces ", 10), ]) def test_string_length(input_val: str, expected: int): assert len(input_val) == expected ``` - **Use descriptive IDs:** `pytest.param("", 0, id="empty-string")` for readable output. - **Each row is a distinct test.** Failures report which parameter combination failed. ### Stacking Decorators ```python @pytest.mark.parametrize("x", [1, 2]) @pytest.mark.parametrize("y", [10, 20]) def test_combinations(x: int, y: int): assert x + y > 0 # Generates: (1,10), (1,20), (2,10), (2,20) ``` ### Indirect Parametrize Pass parameter values to fixtures instead of directly to the test: ```python @pytest.fixture def user(request) -> User: return User(name=request.param) @pytest.mark.parametrize("user", ["Alice", "Bob"], indirect=True) def test_user_greeting(user: User): assert user.name in user.greet() ``` See `${CLAUDE_SKILL_DIR}/references/parametrize.md` for multi-parameter patterns, conditional skipping within parametrize, and dynamic parametrize generation. ## Markers ### Built-in Markers - **`@pytest.mark.skip(reason="...")`** — unconditionally skip. - **`@pytest.mark.skipif(condition, reason="...")`** — skip when condition is true: `@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")`. - **`@pytest.mark.xfail(reason="...")`** — expected failure. Passes if the test fails, reports unexpected pass if it succeeds. Use `strict=True` to fail on unexpected pass. - **`@pytest.mark.usefixtures("fixture_name")`** — inject fixture without using its value. - **`@pytest.mark.filterwarnings("ignore::DeprecationWarning")`** — per-test warning filter. ### Custom Markers Register in `pyproject.toml` to avoid warnings: ```toml [tool.pytest.ini_options] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "integration: marks integration tests", ] ``` ```python @pytest.mark.slow def test_full_pipeline(): ... ``` Run subsets: `pytest -m "not slow"`, `pytest -m "integration and not slow"`. ## Mocking ### monkeypatch (Preferred for Simple Cases) ```python def test_reads_env_variable(monkeypatch): monkeypatch.setenv("API_KEY", "test-key") assert get_api_key() == "test-key" def test_overrides_attribute(monkeypatch): monkeypatch.setattr("myapp.config.DEBUG", True) assert is_debug_mode() is True ``` - **`monkeypatch`** auto-restores on test exit. No manual cleanup. - **Use for:** environment variables, module attributes, dictionary entries, `sys.path`. ### unittest.mock (For Complex Mocking) ```python from unittest.mock import MagicMock, patch, AsyncMock def test_service_calls_repository(): repo = MagicMock(spec=UserRepository) repo.get.return_value = User(name="Alice") service = UserService(repo=repo) result = service.find_user("alice") repo.get.assert_called_once_with("alice") assert result.name == "Alice" @patch("myapp.services.httpx.get") def test_fetches_external_data(mock_get): mock_get.return_value = MagicMock(json=lambda: {"status": "ok"}) assert fetch_status() == "ok" ``` - **Always use `spec=`** on MagicMock — catches attribute typos at test time. - **`AsyncMock`** for async functions. Auto-detected when patching async targets. - **`patch` target is where the name is looked up**, not where it's defined: `@patch("myapp.services.httpx.get")` not `@patch("httpx.get")`. ### pytest-mock (mocker Fixture) ```python def test_with_mocker(mocker): mock_fetch = mocker.patch("myapp.services.fetch_data") mock_fetch.return_value = {"key": "value"} result = process_data() mock_fetch.assert_called_once() ``` - **`mocker` auto-restores** after each test. Prefer over manual `patch` context managers. - **`mocker.patch("module.Class", autospec=True)`** — recursively specs all attributes and method signatures from the real object. Catches signature mismatches at test time. - **`mocker.spy(obj, "method")`** wraps the real method — tracks calls while preserving behavior. ### Mocking Rules - **Mock at boundaries.** Mock external services, databases, filesystems, clocks — not internal functions. - **Don't mock what you own** when a fake or in-memory implementation is available. - **Prefer dependency injection** over patching. Pass collaborators as parameters, mock in tests. - **Never mock the thing you're testing.** If you need to mock part of the SUT, the SUT has too many responsibilities — split it. ## Assertions ### Plain Assert pytest rewrites `assert` statements to show detailed failure messages: ```python assert result == expected # shows both values on failure assert "error" in message # shows the full string assert len(items) == 3 # shows actual length assert all(x > 0 for x in values) # shows the values ``` - **No assertion library needed.** Plain `assert` with pytest's rewrite engine gives clear failure messages. - **Multiple assertions per test are fine** when they verify the same behavior. ### Exception Testing ```python def test_raises_on_invalid_input(): with pytest.raises(ValueError, match=r"must be positive"): calculate(-1) def test_exception_attributes(): with pytest.raises(ValidationError) as exc_info: validate(bad_data) assert exc_info.value.field == "email" assert "invalid format" in str(exc_info.value) ``` - **Always use `match=`** when the exception type is broad — validates the message. - **Access `.value`** for exception attributes via `exc_info`. - **`pytest.raises` is a context manager.** The code that raises must be inside the `with`. ### Approximate Comparisons ```python assert result == pytest.approx(3.14, abs=0.01) assert results == pytest.approx([1.0, 2.0, 3.0], rel=1e-3) ``` ### Warning Testing ```python def test_deprecation_warning(): with pytest.warns(DeprecationWarning, match="use new_func"): old_func() ``` ## Async Testing With `pytest-asyncio`: ```python import pytest import pytest_asyncio @pytest.mark.asyncio
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub