pytest-advanced — Claude Code skill: fixtures, markers, coverage
A skill for advanced pytest work: scoped fixtures, parametrization, parallel runs and coverage. Useful once a test suite grows slow.
npx skills add laurigates/claude-plugins --skill pytest-advanced -a claude-codeWhat pytest-advanced does
The skill covers the parts of pytest that basic docs skip: fixtures scoped to function, class, module or session, factory fixtures that clean up after themselves, and indirect parametrization through request.param. Markers get their own section — skip, skipif, xfail, timeout — plus how to select tests with pytest -m. A plugin table lists pytest-cov, pytest-xdist, pytest-asyncio, pytest-mock and pytest-timeout with the command each one needs.
Configuration lives in pyproject.toml under [tool.pytest.ini_options]: test paths, --strict-markers, --cov=src, a coverage floor of --cov-fail-under=80, and asyncio_mode = "auto". Branch coverage and exclusion lines like pragma: no cover are set there too. Dependencies install with uv add --dev pytest pytest-cov pytest-asyncio pytest-xdist pytest-mock — on a project without uv you rewrite that line for whatever package manager you use.
The last section is a command table for common situations: a quick check, stop on first failure, parallel runs with -n auto, re-running only failures with --lf, debugging with --pdb. CI gets a GitHub Actions step using --junitxml and --cov-report=xml. You do not invoke this skill yourself: it carries user-invocable: false, so the agent picks it up when a task touches test infrastructure.
What it helps with
- write a fixture with the right scope and teardown
- run one test against sqlite, postgres and mysql through parametrization
- spread a slow suite across cores with pytest-xdist
- set a coverage floor and branch coverage in pyproject.toml
- test async functions without hand-rolling an event loop
Example prompts for the agent
- Write a session-scoped fixture for the test app
- Set up pytest in pyproject.toml with strict markers and an 80 coverage floor
- Parametrize this test across three database backends
How to install pytest-advanced
1Skills CLI (Vercel)
npx skills add laurigates/claude-plugins --skill pytest-advanced -a claude-code2Manually: put the files in place
~/.claude/skills/pytest-advanced/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/pytest-advanced/
1Skills CLI (Vercel)
npx skills add laurigates/claude-plugins --skill pytest-advanced -a codex2Manually: put the files in place
.agents/skills/pytest-advanced/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add laurigates/claude-plugins --skill pytest-advanced -a cursor2Manually: put the files in place
.cursor/skills/pytest-advanced/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install laurigates/claude-plugins pytest-advanced2Manually: put the files in place
.github/skills/pytest-advanced/SKILL.md1Skills CLI (Vercel)
npx skills add laurigates/claude-plugins --skill pytest-advanced -a gemini-cli2Platform CLI
gemini skills install https://github.com/laurigates/claude-plugins --path python-plugin/skills/pytest-advancedManually: put the files in place
.devin/skills/pytest-advanced/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add laurigates/claude-plugins --skill pytest-advanced -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about pytest-advanced
How do I install pytest-advanced in Claude?
Run npx skills add laurigates/claude-plugins --skill pytest-advanced -a claude-code. The skill file lands at ~/.claude/skills/pytest-advanced/SKILL.md.
What does pytest-advanced do?
It documents advanced pytest patterns: scoped and factory fixtures, parametrization, markers, async tests and parallel execution. The agent reaches for it when you build or rework test infrastructure.
Do I need to invoke the skill manually?
No. It is marked user-invocable: false, so the agent loads it on its own when a task involves fixtures, markers, parametrization or coverage runs.
Source text
SKILL.md · MIT
---
created: 2025-12-16
modified: 2026-05-09
reviewed: 2026-01-24
name: pytest-advanced
description: "Advanced pytest: fixtures, markers, parametrization, parallel execution. Use when implementing test infrastructure, writing fixtures, or running with coverage."
user-invocable: false
allowed-tools: Bash(pytest *), Bash(python *), Bash(uv run *), Read, Grep, Glob
---
# Advanced Pytest Patterns
Advanced pytest features for robust, maintainable test suites.
## When to Use This Skill
| Use this skill when... | Use python-testing instead when... |
|------------------------|-------------------------------------|
| Writing fixtures with scopes/factories | Basic test structure questions |
| Parametrizing with complex data | Simple assert patterns |
| Setting up pytest plugins (cov, xdist) | Running existing tests |
| Organizing conftest.py hierarchy | Test discovery basics |
| Async testing with pytest-asyncio | Synchronous unit tests |
## Installation
```bash
# Core + common plugins
uv add --dev pytest pytest-cov pytest-asyncio pytest-xdist pytest-mock
```
## Configuration (pyproject.toml)
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
"-ra",
"--cov=src",
"--cov-report=term-missing",
"--cov-fail-under=80",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
]
asyncio_mode = "auto"
[tool.coverage.run]
branch = true
source = ["src"]
[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "@abstractmethod"]
```
## Fixtures
### Scopes and Lifecycle
```python
import pytest
from typing import Generator
# function (default) - fresh per test
@pytest.fixture
def db() -> Generator[Database, None, None]:
database = Database(":memory:")
database.create_tables()
yield database
database.close()
# session - shared across all tests
@pytest.fixture(scope="session")
def app():
return create_app("testing")
# autouse - applies automatically
@pytest.fixture(autouse=True)
def reset_state():
clear_cache()
yield
```
| Scope | Lifetime |
|-------|----------|
| `function` | Each test (default) |
| `class` | Each test class |
| `module` | Each test file |
| `session` | Entire test run |
### Parametrized Fixtures
```python
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_backend(request) -> str:
return request.param # Test runs 3 times
# Indirect parametrization
@pytest.fixture
def user(request) -> User:
return User(**request.param)
@pytest.mark.parametrize("user", [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
], indirect=True)
def test_user_validation(user: User):
assert user.name
```
### Factory Pattern
```python
@pytest.fixture
def user_factory() -> Callable[[str], User]:
created: list[User] = []
def _create(name: str, **kwargs) -> User:
user = User(name=name, **kwargs)
created.append(user)
return user
yield _create
for u in created:
u.delete()
```
## Markers
```python
# Built-in markers
@pytest.mark.skip(reason="Not implemented")
@pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires 3.12+")
@pytest.mark.xfail(reason="Known bug #123")
@pytest.mark.timeout(10)
# Parametrize
@pytest.mark.parametrize("input,expected", [
pytest.param(2, 4, id="two"),
pytest.param(3, 9, id="three"),
pytest.param(-2, 4, id="negative"),
])
def test_square(input: int, expected: int):
assert input ** 2 == expected
```
```bash
# Run by marker
pytest -m unit # Only unit tests
pytest -m "not slow" # Skip slow tests
pytest -m "integration and not slow" # Combine markers
```
## Key Plugins
| Plugin | Purpose | Key Command |
|--------|---------|-------------|
| pytest-cov | Coverage | `pytest --cov=src --cov-report=term-missing` |
| pytest-xdist | Parallel | `pytest -n auto` |
| pytest-asyncio | Async tests | `asyncio_mode = "auto"` in config |
| pytest-mock | Mocking | `mocker` fixture |
| pytest-timeout | Timeouts | `@pytest.mark.timeout(10)` |
### Async Testing (pytest-asyncio)
```python
@pytest.mark.asyncio
async def test_async_function():
result = await fetch_data()
assert result is not None
@pytest.fixture
async def async_client() -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient() as client:
yield client
```
### Mocking (pytest-mock)
```python
def test_with_mock(mocker):
mock_api = mocker.patch("myapp.external.api_call")
mock_api.return_value = {"data": "test"}
result = my_function()
assert result["data"] == "test"
mock_api.assert_called_once()
```
## Running Tests
```bash
# Execution
pytest # All tests
pytest tests/test_models.py::test_user # Specific test
pytest -k "user and not slow" # Pattern matching
# Parallel
pytest -n auto # All CPUs
pytest -n 4 # 4 workers
# Coverage
pytest --cov=src --cov-report=html --cov-report=term-missing
# Failed tests
pytest --lf # Last failed only
pytest --ff # Failed first
pytest -x # Stop on first failure
pytest --maxfail=3 # Stop after 3
# Debugging
pytest -x --pdb # Debug on failure
pytest -s # Show print output
pytest --collect-only # Dry run
```
## CI Integration
```yaml
# .github/workflows/test.yml
- name: Run tests
run: |
uv run pytest \
--cov=src \
--cov-report=xml \
--cov-report=term-missing \
--junitxml=test-results.xml
```
## Agentic Optimizations
| Context | Command |
|---------|---------|
| Quick check | `pytest -x --tb=short -q` |
| Fail fast | `pytest -x --maxfail=1 --tb=short` |
| Parallel fast | `pytest -n auto -x --tb=short -q` |
| Specific test | `pytest tests/test_foo.py::test_bar -v` |
| By marker | `pytest -m "not slow" -x --tb=short` |
| Coverage check | `pytest --cov=src --cov-fail-under=80 -q` |
| CI mode | `pytest --junitxml=results.xml --cov-report=xml -q` |
| Last failed | `pytest --lf --tb=short` |
| Debug | `pytest -x --pdb -s` |
For detailed patterns on conftest.py hierarchy, async testing, test organization, and common patterns, see [REFERENCE.md](REFERENCE.md).
© laurigates · MIT · laurigates/claude-plugins