Claude
Skills
Sign in
Back

pytest

Included with Lifetime
$97 forever

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.

Productivity

What this skill does


# 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": "[email protected]"}

    # 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

Related in Productivity