Pytest
AI skills
npx -y skills add kpatryk/skills --skill pytestAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert guidance for writing, organizing, debugging, and running Python tests with pytest. Covers fixtures, parametrize, marks, mocking (monkeypatch + pytest-mock), async tests, output capture, conftest.py patterns, and plugins (pytest-cov, pytest-asyncio, pytest-xdist). Use this skill whenever the user is writing Python tests, setting up test infrastructure, debugging failing tests, adding pytest configuration in pyproject.toml, working with conftest.py, or using any pytest plugin. Invoke it even when the user just says "write me a test" or "my test is failing" or "add coverage" — if it involves Python testing, this skill applies.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
11.6 KB, as published. Nobody here has run it
pytest
Python's most popular testing framework. Plain assert statements with rich failure diffs,
powerful fixtures, parametrization, and a vast plugin ecosystem.
Test Discovery
pytest auto-discovers tests without any registration:
- Files:
test_*.pyor*_test.py - Functions/methods: names starting with
test_ - Classes: names starting with
Test(no__init__method)
CLI Quick Reference
pytest # run all tests
pytest tests/test_foo.py # specific file
pytest tests/test_foo.py::test_bar # specific test
pytest -v / -vv # verbose output
pytest -q # quiet (dot per test)
pytest -k "auth and not slow" # filter by name expression
pytest -m slow # filter by mark
pytest -x # stop on first failure
pytest --maxfail=3 # stop after N failures
pytest --lf # rerun last-failed only
pytest --ff # run failed-first
pytest -s # disable output capture (show prints)
pytest --tb=short # traceback style: short/long/no/line/native/auto
pytest --co # collect only (list tests, don't run)
pytest --durations=10 # show 10 slowest tests
pytest -r a # show extra summary (a=all, f=failed, s=skipped)
pytest -p no:warnings # disable warnings plugin
pytest -n auto # parallel execution (pytest-xdist)
pytest --cov=src --cov-report=term-missing # coverage (pytest-cov)
pytest --asyncio-mode=auto # auto async mode (pytest-asyncio)
Writing Tests
Use plain assert — pytest rewrites assertions to show rich diffs on failure:
def test_addition():
assert 1 + 1 == 2
def test_raises():
with pytest.raises(ValueError, match="invalid"):
raise ValueError("invalid input")
def test_approx():
assert 0.1 + 0.2 == pytest.approx(0.3)
Never use assertEqual, assertTrue, etc. — pytest's assert rewrites give better output.
Fixtures
Fixtures provide reusable setup. Request them by adding as function parameters:
import pytest
@pytest.fixture
def db_conn():
conn = create_connection(":memory:")
yield conn # everything after yield is teardown
conn.close()
def test_query(db_conn):
result = db_conn.execute("SELECT 1").fetchone()
assert result == (1,)
Fixture Scope
Controls how often a fixture is instantiated:
@pytest.fixture(scope="function") # default: new instance per test
@pytest.fixture(scope="class") # shared across a test class
@pytest.fixture(scope="module") # shared across a test file
@pytest.fixture(scope="package") # shared across a package
@pytest.fixture(scope="session") # shared across the entire test run
Fixture Parameters
@pytest.fixture(
scope="module",
autouse=False, # True = applied automatically to all tests in scope
name="client", # alias — lets you use a different name than the function
params=["a", "b"], # parametrize the fixture itself
)
def http_client(request):
return make_client(base_url=request.param)
Built-in Fixtures
| Fixture | Purpose |
|---|---|
tmp_path | pathlib.Path to a per-test temp dir — prefer over /tmp |
tmp_path_factory | Session-scoped temp dirs |
capsys | Capture/assert on stdout/stderr: capsys.readouterr() |
capfd | Like capsys but at file-descriptor level |
caplog | Capture log records: caplog.records, caplog.text |
monkeypatch | Safe attribute/env/dict patching with auto-undo |
request | Introspect the current test (param, node, config) |
pytestconfig | Access pytest config / CLI options |
mocker | pytest-mock: mocker.patch(...), mocker.MagicMock() |
conftest.py
Place fixtures (and hooks) shared across multiple test files in conftest.py. pytest discovers it automatically — no imports needed:
tests/
conftest.py ← fixtures available to all tests/
unit/
conftest.py ← fixtures available to unit/ only
test_foo.py
integration/
test_bar.py
Marks
Built-in Marks
@pytest.mark.skip(reason="not implemented yet")
def test_future(): ...
@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires 3.11+")
def test_new_feature(): ...
@pytest.mark.xfail(strict=False, raises=NotImplementedError, reason="known bug")
def test_buggy(): ...
@pytest.mark.usefixtures("db_conn", "clean_cache")
class TestUserFlow:
def test_login(self): ...
Custom Marks
Register in pyproject.toml to avoid warnings (and optionally enforce with --strict-markers):
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: requires external services",
"smoke: critical path tests only",
]
addopts = ["--strict-markers"]
Then use: @pytest.mark.slow, @pytest.mark.integration, etc.
Parametrize
Run the same test with multiple inputs — each combination becomes a separate test case:
@pytest.mark.parametrize("x, expected", [
(1, 2),
(5, 6),
(-1, 0),
], ids=["one", "five", "negative"])
def test_increment(x, expected):
assert x + 1 == expected
# Stack decorators for cartesian product
@pytest.mark.parametrize("base", [2, 10])
@pytest.mark.parametrize("exp", [0, 1, 2])
def test_power(base, exp):
assert base ** exp >= 0
# Mark individual cases
@pytest.mark.parametrize("val", [
1,
pytest.param(-1, marks=pytest.mark.xfail(reason="negative not supported")),
])
def test_sqrt(val):
assert math.sqrt(val) >= 0
Mocking
monkeypatch (built-in, no extra install)
def test_env_var(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key")
monkeypatch.delenv("SECRET", raising=False)
assert os.environ["API_KEY"] == "test-key"
def test_patch_function(monkeypatch):
monkeypatch.setattr("myapp.utils.fetch_data", lambda url: {"ok": True})
result = myapp.utils.fetch_data("http://example.com")
assert result == {"ok": True}
def test_patch_dict(monkeypatch):
monkeypatch.setitem(config, "timeout", 0)
All patches are automatically reverted after each test — no cleanup needed.
pytest-mock (install: pip install pytest-mock)
Wraps unittest.mock with a pytest-idiomatic API via the mocker fixture:
def test_api_call(mocker):
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
result = fetch_user(1)
assert result["name"] == "Alice"
mock_get.assert_called_once_with("https://api.example.com/users/1")
def test_with_spy(mocker):
spy = mocker.spy(my_module, "expensive_function")
do_work()
assert spy.call_count == 1
Prefer mocker.patch over unittest.mock.patch as a decorator/context manager — it integrates with pytest's fixture lifecycle.
Async Tests
With pytest-asyncio (pip install pytest-asyncio):
# pyproject.toml
# [tool.pytest.ini_options]
# asyncio_mode = "auto"
import pytest
@pytest.mark.asyncio # not needed when asyncio_mode = "auto"
async def test_async_endpoint():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/health")
assert response.status_code == 200
@pytest.fixture
async def async_db():
db = await create_async_db()
yield db
await db.close()
Output Capture
def test_stdout(capsys):
print("hello world")
captured = capsys.readouterr()
assert captured.out == "hello world\n"
assert captured.err == ""
def test_logging(caplog):
import logging
with caplog.at_level(logging.WARNING):
logging.warning("watch out")
assert "watch out" in caplog.text
Use -s / --capture=no on the CLI to see live output during debugging.
Configuration (pyproject.toml)
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"--strict-markers",
"-v",
"--tb=short",
]
markers = [
"slow: deselect with '-m \"not slow\"'",
"integration: needs external services",
]
asyncio_mode = "auto"
filterwarnings = [
"error",
"ignore::DeprecationWarning:third_party_lib",
]
log_cli = true
log_cli_level = "INFO"
Key Plugins
| Plugin | Install | Purpose |
|---|---|---|
pytest-cov | pip install pytest-cov | Coverage reports |
pytest-mock | pip install pytest-mock | mocker fixture wrapping unittest.mock |
pytest-asyncio | pip install pytest-asyncio | async/await test support |
pytest-xdist | pip install pytest-xdist | Parallel execution with -n auto |
pytest-httpx | pip install pytest-httpx | Mock httpx requests |
pytest-freezegun | pip install pytest-freezegun | Freeze time in tests |
pytest-django | pip install pytest-django | Django database + client fixtures |
Coverage
pytest --cov=src --cov-report=term-missing # console with missing lines
pytest --cov=src --cov-report=html:htmlcov # HTML report
pytest --cov=src --cov-report=xml # XML for CI
pytest --cov=src --cov-fail-under=90 # fail if coverage < 90%
Common Patterns
Fixture with Teardown (yield)
@pytest.fixture
def temp_file(tmp_path):
f = tmp_path / "data.txt"
f.write_text("initial content")
yield f
# teardown: tmp_path cleaned up automatically, but you can do more here
Parametrized Fixture
@pytest.fixture(params=["sqlite", "postgres"])
def database(request):
db = connect(request.param)
yield db
db.disconnect()
def test_insert(database): # runs twice, once per db engine
database.insert({"key": "value"})
assert database.count() == 1
Class-based Tests (no __init__)
class TestUserAuth:
def test_login_valid(self, client, valid_user):
resp = client.post("/login", json=valid_user)
assert resp.status_code == 200
def test_login_invalid(self, client):
resp = client.post("/login", json={"user": "x", "pass": "wrong"})
assert resp.status_code == 401
Common Pitfalls
| Pitfall | Fix |
|---|---|
PytestUnknownMarkWarning | Register marks in pyproject.toml markers list |
| Fixture scope mismatch | A function-scoped fixture can't request a session-scoped fixture's state safely — match or widen scope |
ScopeMismatch error | A wider-scope fixture (e.g. session) can't request a narrower-scope fixture (e.g. function) |
asyncio_mode not set | Add asyncio_mode = "auto" to [tool.pytest.ini_options] or use @pytest.mark.asyncio |
| Importing conftest | Never import conftest — pytest injects fixtures automatically |
Writing to /tmp directly | Use tmp_path fixture — it's isolated, cleaned up, and shows in test output |
Using unittest.mock.patch as decorator | Use mocker.patch(...) from pytest-mock so patches respect fixture lifecycle |
Missing pytest.raises context | If testing that code raises, use with pytest.raises(ExcType): not a bare try/except |