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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- pytest-testing Installs to ~/.agents/skills/pytest-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install pytest-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project pytest-testing Installs to .agents/skills/pytest-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/pytest-testing.zipunzip -o pytest-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) pytest-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are testing HTTP endpoints end to end with httpx: use api-testing
- You want generated inputs and shrinking with Hypothesis: use property-based-testing
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.
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
- Read
AGENTS.mdfor the test command (uv run pytest,poetry run pytest,tox,nox). Use exactly that; plainpytestmay pick the wrong interpreter. - Find config:
[tool.pytest.ini_options]inpyproject.toml,pytest.ini,tox.iniorsetup.cfg. Only one is used; pytest printsconfigfile:in the header. - List fixtures available to a test directory:
pytest --fixtures tests/api -q. - 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-markersturns a typo like@pytest.mark.slwointo 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=importlibavoidssys.pathmanipulation and duplicate basename clashes; with it, test modules cannot import each other, so shared helpers go inconftest.pyor an importable package.
Fixtures
| State the fixture holds | Scope |
|---|---|
| Mutable objects, DB rows, temp files, mocks | function (default) |
| Expensive read-only resource (compiled schema, parsed large file) | module or session |
| External process or container started once | session, 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.pyclosest to the tests that use them. Rootconftest.pyis for project-wide fixtures and hooks only. autouse=Trueonly 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
yieldruns even when the test fails; code after areturnnever 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 readstest_parse_amount[thousands-separator]and-k thousandsselects it. - Stacked
@pytest.mark.parametrizedecorators produce the cartesian product; keep it small. indirect=["user"]routes a parameter through theuserfixture asrequest.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
| Fixture | Use for |
|---|---|
tmp_path | per-test pathlib.Path directory |
monkeypatch | setenv, setattr, chdir; undone after the test |
capsys / capfd | captured stdout/stderr |
caplog | log records: caplog.set_level(logging.INFO), then caplog.records |
request | parameters, 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,
sessionfixtures run once per worker. Shared external resources (one test database) need per-worker names: use theworker_idfixture to suffix them. - pytest-randomly prints the seed in the header; reproduce an ordering failure with
--randomly-seed=<n>, or--randomly-seed=lastto reuse the previous run’s seed.
Gotchas
- Tests pass alone, fail together: module-level state, a fixture with too wide a scope,
or
monkeypatchreplaced by direct assignment. Run with-p no:randomlyand bisect the order, or run the pairpytest tests/a.py::test_x tests/b.py::test_y. assertinside a helper outside test modules loses assertion rewriting; register the helper module withpytest.register_assert_rewrite("tests.helpers")inconftest.py.returninstead ofyieldin a fixture silently skips teardown.- A test function named
test_*that returns a value triggers a warning, and withfilterwarnings = errora failure; tests should assert, not return. - Class-based tests: no
__init__; use fixtures, notsetUp, unless the suite is unittest.
Verification
- Run the touched tests, then the whole suite with the project command.
- Run touched tests with random order (if pytest-randomly is installed) and with
-n autoto expose shared state. - Check the summary line from
-ra: no unexpected skips or xpasses. - 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.yaml
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.