pytest Testing skill for Codex

Fixtures scoped right, cases named, suites parallel. Codex writes pytest suites that hold up under random order and parallel workers. It scopes fixtures by the state they hold, layers conftest.py files, names parametrized cases so failures read clearly, patches where names are looked up, and turns warnings and marker typos into errors so problems surface early.

  • Testing & QA
  • $pytest-testing
  • 206 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- pytest-testing

Installs to ~/.agents/skills/pytest-testing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $pytest-testing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and organizes pytest suites - fixtures with the right scope and yield teardown, conftest.py layering, parametrize with ids and indirect fixtures, markers, monkeypatch, tmp_path, caplog, pytest-asyncio, pytest-xdist and pytest-cov, plus a strict pyproject configuration. Use when a Python project tests with pytest, when fixtures leak state between tests, tests pass alone but fail together, parametrized cases are unreadable, or the suite is slow and needs parallelism.

Use it when

  • A Python project uses or is moving to pytest
  • Fixtures leak state or have scope errors
  • Parametrized tests are hard to read or select
  • The suite needs parallelism, coverage gates or async support

Use something else when

How to check the result

The suite passes with pytest-randomly enabled and with -n auto, the -ra summary shows no unexpected skips, and strict markers and warnings-as-errors stay on.

The mistake it prevents

Patching a function where it is defined instead of where the code under test imported it, so the real function still runs.

~/.agents/skills/pytest-testing/SKILL.md

--- name: pytest-testing description: Writes and organizes pytest suites - fixtures with the right scope and yield teardown, conftest.py layering, parametrize with ids and indirect… license: MIT ---

A well-kept pytest suite has strict config (unknown markers and warnings fail), fixtures scoped no wider than their state allows, parametrized cases with readable ids, and passes under random order and -n auto.

Inspect first

  1. Read AGENTS.md for the test command (uv run pytest, poetry run pytest, tox, nox). Use exactly that; plain pytest may pick the wrong interpreter.
  2. Find config: [tool.pytest.ini_options] in pyproject.toml, pytest.ini, tox.ini or setup.cfg. Only one is used; pytest prints configfile: in the header.
  3. List fixtures available to a test directory: pytest --fixtures tests/api -q.
  4. List installed plugins from the run header (plugins: ...). Do not add a plugin that duplicates one already present.

Baseline configuration

[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers", "--strict-config", "--import-mode=importlib"]
xfail_strict = true
filterwarnings = ["error", "ignore::DeprecationWarning:some_third_party.*"]
markers = [
  "slow: takes more than a second; deselect with -m 'not slow'",
  "integration: needs external services",
]
asyncio_mode = "auto"          # only if pytest-asyncio is installed
  • --strict-markers turns a typo like @pytest.mark.slwo into an error instead of a silently unselected test.
  • filterwarnings = ["error"] surfaces deprecations before they become breakages; add narrow ignores for third-party noise, never a blanket ignore.
  • --import-mode=importlib avoids sys.path manipulation and duplicate basename clashes; with it, test modules cannot import each other, so shared helpers go in conftest.py or an importable package.

Fixtures

State the fixture holdsScope
Mutable objects, DB rows, temp files, mocksfunction (default)
Expensive read-only resource (compiled schema, parsed large file)module or session
External process or container started oncesession, with per-test cleanup in a function fixture
import pytest

@pytest.fixture(scope="session")
def engine():
    eng = create_engine(TEST_DB_URL)
    yield eng
    eng.dispose()

@pytest.fixture
def db(engine):
    with engine.connect() as conn:
        tx = conn.begin()
        yield Session(bind=conn)
        tx.rollback()                     # each test sees a clean database

Factory fixture when a test needs several variations:

@pytest.fixture
def make_user(db):
    created = []
    def _make(**overrides):
        user = User(email=f"u{len(created)}@example.com", **overrides)
        db.add(user); db.flush()
        created.append(user)
        return user
    return _make

Rules:

  • A wider-scoped fixture cannot depend on a narrower one (ScopeMismatch).
  • Put fixtures in the conftest.py closest to the tests that use them. Root conftest.py is for project-wide fixtures and hooks only.
  • autouse=True only for things every test truly needs (resetting a global cache). Hidden autouse fixtures are the top cause of “why is this test slow”.
  • Teardown after yield runs even when the test fails; code after a return never runs.

Parametrize

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        pytest.param("10.00", Decimal("10.00"), id="plain"),
        pytest.param("1,000.50", Decimal("1000.50"), id="thousands-separator"),
        pytest.param("", None, id="empty"),
        pytest.param("abc", None, id="garbage", marks=pytest.mark.xfail(reason="raises today")),
    ],
)
def test_parse_amount(raw, expected):
    assert parse_amount(raw) == expected
  • Always give id= when values are objects or long strings; failure output then reads test_parse_amount[thousands-separator] and -k thousands selects it.
  • Stacked @pytest.mark.parametrize decorators produce the cartesian product; keep it small.
  • indirect=["user"] routes a parameter through the user fixture as request.param, useful when each case needs setup:
@pytest.fixture
def user(request, make_user):
    return make_user(role=request.param)

@pytest.mark.parametrize("user", ["admin", "viewer"], indirect=True)
def test_dashboard_access(user, client): ...

Built-in fixtures worth using

FixtureUse for
tmp_pathper-test pathlib.Path directory
monkeypatchsetenv, setattr, chdir; undone after the test
capsys / capfdcaptured stdout/stderr
caploglog records: caplog.set_level(logging.INFO), then caplog.records
requestparameters, markers, request.addfinalizer
def test_reads_api_key_from_env(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key")
    assert load_settings().api_key == "test-key"

def test_rejects_negative_amount():
    with pytest.raises(ValueError, match=r"amount must be >= 0"):
        Payment(amount=-1)

Use pytest.approx for floats, never ==.

Async tests

With pytest-asyncio 1.x and asyncio_mode = "auto", async def tests and fixtures just work. The old event_loop fixture is gone; set loop scope with @pytest.mark.asyncio(loop_scope="module") when a module-scoped async fixture shares a loop with its tests. For anyio-based code, use the anyio plugin instead and do not mix both.

Mocking

Prefer dependency injection and fakes. When patching is needed, patch where the name is looked up, not where it is defined:

# app/billing.py does: from app.gateway import charge
def test_retries_on_timeout(mocker):           # pytest-mock
    charge = mocker.patch("app.billing.charge", side_effect=[TimeoutError, {"ok": True}])
    assert bill(order).ok
    assert charge.call_count == 2

mocker.patch(..., autospec=True) keeps signatures honest; a wrong argument count fails.

Selection, speed and parallelism

pytest -x --lf                         # stop at first failure, rerun last failures
pytest -k "parse and not garbage"      # by name expression
pytest -m "not slow and not integration"
pytest --durations=15                  # slowest setup/call/teardown phases
pytest -n auto                         # pytest-xdist
pytest -n auto --dist loadscope        # keep a module's tests on one worker
pytest -p no:randomly                  # disable pytest-randomly to reproduce an order
pytest --cov=src/app --cov-branch --cov-report=term-missing --cov-fail-under=85
  • With xdist, session fixtures run once per worker. Shared external resources (one test database) need per-worker names: use the worker_id fixture to suffix them.
  • pytest-randomly prints the seed in the header; reproduce an ordering failure with --randomly-seed=<n>, or --randomly-seed=last to reuse the previous run’s seed.

Gotchas

  • Tests pass alone, fail together: module-level state, a fixture with too wide a scope, or monkeypatch replaced by direct assignment. Run with -p no:randomly and bisect the order, or run the pair pytest tests/a.py::test_x tests/b.py::test_y.
  • assert inside a helper outside test modules loses assertion rewriting; register the helper module with pytest.register_assert_rewrite("tests.helpers") in conftest.py.
  • return instead of yield in a fixture silently skips teardown.
  • A test function named test_* that returns a value triggers a warning, and with filterwarnings = error a failure; tests should assert, not return.
  • Class-based tests: no __init__; use fixtures, not setUp, unless the suite is unittest.

Verification

  1. Run the touched tests, then the whole suite with the project command.
  2. Run touched tests with random order (if pytest-randomly is installed) and with -n auto to expose shared state.
  3. Check the summary line from -ra: no unexpected skips or xpasses.
  4. Confirm a new test fails when the behavior it checks is broken.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "pytest Testing"
  short_description: "Fixtures scoped right, cases named, suites parallel"
  default_prompt: "$pytest-testing Refactor these tests to use pytest fixtures and parametrize"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc