Skip to content
Back to skills

Pytest Testing

ASecurity

pytest testing patterns for any Python project: test structure, fixtures, parametrize, conftest.py, monkeypatch, tmp_path, markers, unittest.mock (MagicMock, patch), pytest-cov coverage, and best practices. Stack-agnostic — referenced by every Python plugin in the marketplace. Use this skill to: - Organise tests with conftest.py shared fixtures and a clear test/src layout. - Write parametrized tests to cover multiple inputs without duplication. - Mock external dependencies (HTTP, DB, filesys...

  • 35 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
developmentpythongobashsqlexpressfastapidjangoflaskdockertesting

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 22, 2026

npx -y skills add AratKruglik/claude-sdlc --skill pytest-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pytest Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pytest Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aratkruglik-pytest-testing/badge)](https://www.skillsdirectory.com/skills/aratkruglik-pytest-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pytest-testing
description: |
  pytest testing patterns for any Python project: test structure, fixtures, parametrize, conftest.py, monkeypatch, tmp_path, markers, unittest.mock (MagicMock, patch), pytest-cov coverage, and best practices. Stack-agnostic — referenced by every Python plugin in the marketplace.

  Use this skill to:
  - Organise tests with conftest.py shared fixtures and a clear test/src layout.
  - Write parametrized tests to cover multiple inputs without duplication.
  - Mock external dependencies (HTTP, DB, filesystem) with unittest.mock and monkeypatch.
  - Measure and enforce code coverage with pytest-cov.

  Do NOT use this skill for:
  - Framework-specific test types (Django TestCase / WebTestCase, FastAPI TestClient, Flask test client — see framework plugin skills).
  - Language idioms — see python-foundation:python-conventions.
user-invocable: false
paths: ["tests/**", "**/test_*.py", "**/*_test.py", "**/conftest.py"]
---

# pytest Testing Patterns (stack-agnostic)

pytest is the standard test runner for modern Python projects. These patterns apply to any Python 3.10+ codebase regardless of framework.

## Project layout

Keep tests outside the source tree so installed packages are tested rather than the raw source directory:

```
src/
  myapp/
    __init__.py
    service.py
    repository.py
tests/
  conftest.py
  test_service.py
  test_repository.py
  integration/
    conftest.py
    test_db.py
pyproject.toml
```

Configure pytest to find the source in `pyproject.toml`:

```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--strict-markers -q"
```

Install the package in editable mode before running tests so `import myapp` resolves correctly:

```bash
pip install -e .        # or: poetry install / uv sync
pytest
```

## conftest.py — shared fixtures

`conftest.py` files are loaded automatically by pytest. Place shared fixtures there instead of importing them into every test module.

```python
# tests/conftest.py
from __future__ import annotations

import pytest

from myapp.database import Database, get_connection
from myapp.service import UserService


@pytest.fixture(scope="module")
def db() -> Database:
    """Module-scoped DB connection — created once per test module."""
    conn = get_connection(url="sqlite:///:memory:")
    conn.create_tables()
    yield conn
    conn.close()


@pytest.fixture
def user_service(db: Database) -> UserService:
    """Function-scoped service — fresh instance per test, shared DB."""
    return UserService(db)
```

Fixture scopes (from broadest to narrowest):

| Scope | Created | Destroyed |
|---|---|---|
| `session` | Once per `pytest` run | After all tests |
| `module` | Once per test module | After last test in module |
| `class` | Once per test class | After last test in class |
| `function` (default) | Once per test function | After the test returns |

Use the narrowest scope that avoids expensive re-creation. Use `session` scope only for truly global state (e.g., a Docker container started via `pytest-docker`).

## Basic test patterns

```python
# tests/test_service.py
from __future__ import annotations

import pytest

from myapp.service import UserService
from myapp.exceptions import NotFoundError


def test_create_user_returns_new_id(user_service: UserService) -> None:
    user_id = user_service.create(name="Alice", email="alice@example.com")
    assert user_id > 0


def test_find_user_by_id(user_service: UserService) -> None:
    user_id = user_service.create(name="Bob", email="bob@example.com")
    user = user_service.find(user_id)
    assert user.name == "Bob"
    assert user.email == "bob@example.com"


def test_find_missing_user_raises(user_service: UserService) -> None:
    with pytest.raises(NotFoundError):
        user_service.find(99999)


def test_find_missing_user_message(user_service: UserService) -> None:
    with pytest.raises(NotFoundError, match="User.*99999"):
        user_service.find(99999)
```

Use plain `assert` — pytest rewrites assertions to produce detailed failure messages. Do not use `assertEqual` / `assertTrue` / `assertRaises` (those are `unittest.TestCase` methods).

## Fixtures — yield, teardown, autouse

Use `yield` in a fixture to run teardown code after the test:

```python
@pytest.fixture
def temp_user(user_service: UserService) -> int:
    user_id = user_service.create(name="Temp", email="temp@example.com")
    yield user_id
    user_service.delete(user_id)   # teardown: always runs even if test fails
```

Fixtures can depend on other fixtures — pytest resolves the dependency graph automatically:

```python
@pytest.fixture
def admin_user(user_service: UserService) -> User:
    user_id = user_service.create(name="Admin", email="admin@example.com")
    return user_service.promote_to_admin(user_id)
```

Use `autouse=True` for cross-cutting setup that every test in a module or package needs:

```python
# tests/integration/conftest.py
@pytest.fixture(autouse=True)
def rollback_after_test(db: Database) -> None:
    """Wrap every integration test in a transaction that rolls back."""
    db.begin()
    yield
    db.rollback()
```

## parametrize — data-driven tests

Drive many input/output combinations through a single test body:

```python
import pytest


@pytest.mark.parametrize("amount,currency,expected_str", [
    (100, "USD", "1.00 USD"),
    (0,   "EUR", "0.00 EUR"),
    (999, "GBP", "9.99 GBP"),
])
def test_money_format(amount: int, currency: str, expected_str: str) -> None:
    money = Money(amount, currency)
    assert str(money) == expected_str
```

Use `pytest.param` to give test cases readable IDs and to mark individual cases:

```python
@pytest.mark.parametrize("raw,expected", [
    pytest.param("draft",    Status.DRAFT,    id="draft"),
    pytest.param("active",   Status.ACTIVE,   id="active"),
    pytest.param("archived", Status.ARCHIVED, id="archived"),
])
def test_status_parsing(raw: str, expected: Status) -> None:
    assert Status(raw) == expected


@pytest.mark.parametrize("raw", [
    pytest.param("unknown", id="unknown-value"),
    pytest.param("",        id="empty-string"),
])
def test_status_invalid(raw: str) -> None:
    with pytest.raises(ValueError):
        Status(raw)
```

Multiple independent parameters are passed as separate arguments — pytest generates all combinations:

```python
@pytest.mark.parametrize("x", [0, 1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_add(x: int, y: int) -> None:
    assert add(x, y) == x + y   # 6 test cases generated
```

## monkeypatch — patching at test time

`monkeypatch` is a pytest fixture that temporarily modifies attributes, environment variables, or dictionary entries and automatically restores them after the test.

```python
import requests


def get_user_count(api_url: str) -> int:
    response = requests.get(f"{api_url}/users/count")
    response.raise_for_status()
    return response.json()["count"]


def test_get_user_count(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeResponse:
        def raise_for_status(self) -> None: ...
        def json(self) -> dict:
            return {"count": 42}

    monkeypatch.setattr(requests, "get", lambda url: FakeResponse())

    count = get_user_count("https://api.example.com")
    assert count == 42
```

Other common `monkeypatch` uses:

```python
def test_reads_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("DATABASE_URL", "sqlite:///:memory:")
    assert get_database_url() == "sqlite:///:memory:"


def test_missing_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("DATABASE_URL", raising=False)
    with pytest.raises(EnvironmentError):
        get_database_url()


def test_patches_dict(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setitem(config, "timeout", 5)
    assert effective_timeout() == 5
```

Prefer `monkeypatch` over `unittest.mock.patch` for patching attributes and environment variables in pytest — it integrates with pytest's fixture lifecycle and does not require a context manager or decorator.

## unittest.mock — mocking objects and calls

Use `unittest.mock` when you need to assert that a collaborator was called with specific arguments, or to make a mock raise an exception.

```python
from unittest.mock import MagicMock, patch

from myapp.service import OrderService


def test_sends_confirmation_email() -> None:
    mailer = MagicMock()
    gateway = MagicMock()
    gateway.charge.return_value = Receipt(id="rcpt_1", amount=500)

    service = OrderService(payment_gateway=gateway, mailer=mailer)
    service.checkout(order=make_order(total_cents=500))

    gateway.charge.assert_called_once_with(500)
    mailer.send.assert_called_once()


def test_checkout_raises_on_payment_failure() -> None:
    gateway = MagicMock()
    gateway.charge.side_effect = PaymentDeclinedError("insufficient funds")

    service = OrderService(payment_gateway=gateway, mailer=MagicMock())

    with pytest.raises(PaymentDeclinedError, match="insufficient funds"):
        service.checkout(order=make_order(total_cents=500))
```

Use `patch` as a context manager when you need to replace a name in a module's namespace:

```python
from unittest.mock import patch


def test_sends_welcome_email() -> None:
    with patch("myapp.service.send_email") as mock_send:
        create_user(name="Alice", email="alice@example.com")
        mock_send.assert_called_once_with(
            to="alice@example.com",
            subject="Welcome",
        )
```

Or as a decorator:

```python
@patch("myapp.service.send_email")
def test_email_on_signup(mock_send: MagicMock) -> None:
    create_user(name="Bob", email="bob@example.com")
    assert mock_send.call_count == 1
```

Patch the name where it is **used**, not where it is defined. If `myapp.service` imports `send_email` from `myapp.email`, patch `myapp.service.send_email`, not `myapp.email.send_email`.

## tmp_path — tests that write files

Use the built-in `tmp_path` fixture (a `pathlib.Path` pointing to a unique temporary directory) for tests that need to read or write files. pytest cleans it up after the session.

```python
from pathlib import Path
import pytest

from myapp.export import write_csv


def test_write_csv_creates_file(tmp_path: Path) -> None:
    output = tmp_path / "report.csv"
    rows = [{"name": "Alice", "score": "95"}, {"name": "Bob", "score": "87"}]

    write_csv(output, rows)

    assert output.exists()
    lines = output.read_text(encoding="utf-8").splitlines()
    assert lines[0] == "name,score"
    assert len(lines) == 3   # header + 2 data rows


def test_load_config_from_file(tmp_path: Path) -> None:
    config_file = tmp_path / "config.toml"
    config_file.write_text('[app]\nport = 8080\n', encoding="utf-8")

    config = load_config(config_file)

    assert config.port == 8080
```

`tmp_path` is function-scoped by default. Use `tmp_path_factory` (session fixture) if multiple fixtures or tests need to share the same temporary directory.

## Coverage — measuring and enforcing

Run pytest with `pytest-cov`:

```bash
pytest --cov=src --cov-report=term-missing
```

Configure coverage in `pyproject.toml`:

```toml
[tool.coverage.run]
source = ["src"]
branch = true               # track branch coverage, not just line coverage

[tool.coverage.report]
fail_under = 80
show_missing = true
omit = ["src/myapp/migrations/*", "src/myapp/__main__.py"]
```

With `fail_under = 80`, `pytest --cov=src` exits non-zero if coverage drops below 80% — useful as a CI gate.

Generate an HTML report for local inspection:

```bash
pytest --cov=src --cov-report=html
open htmlcov/index.html
```

Coverage is a floor, not a goal. A covered line is not necessarily an asserted behaviour — 100% coverage does not mean 0 bugs. Focus on asserting meaningful behaviours; the coverage number confirms you did not miss entire code paths.

## Running tests

```bash
pytest                          # run all tests
pytest -x                       # stop on first failure
pytest -v                       # verbose: show each test name
pytest -q                       # quiet: summary only
pytest -k "test_create"         # run tests whose name matches the expression
pytest tests/test_service.py    # run a specific module
pytest tests/integration/       # run a subdirectory
pytest --tb=short               # shorter tracebacks
pytest --lf                     # run only tests that failed last time
pytest -n auto                  # parallel execution (requires pytest-xdist)
```

Run the full quality pipeline before committing:

```bash
ruff format --check . && ruff check . && mypy src/ && pytest --cov=src
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…