pytest-tests
Configures and runs pytest - the de facto Python test framework with fixture-based dependency injection (`@pytest.fixture` with scopes module/session/function), parametrize for table-driven tests (`@pytest.mark.parametrize`), markers (`@pytest.mark.skip` / `xfail` / `slow`), `conftest.py` for shared fixtures, plugin ecosystem (pytest-cov, pytest-asyncio, pytest-mock, pytest-xdist), `--lf`/`--ff` for fail-loop, coverage gating. Use when working with Python and needing the modern test framework.
Install with skills.sh (any agent)
npx skills add testland/qa --skill pytest-testspytest-tests
Overview
Per docs.pytest.org/en/stable (opens in new window):
pytest is the de facto Python test framework. Unlike stdlib unittest, it uses function-style tests (no TestCase), fixture-based dependency injection, parametrize for data-driven tests, and plain-assert rewriting for diff-rich failures.
Lifecycle scope: configure / run / fixtures / mocking / coverage / CI. Test code hygiene (assertions, AAA, mocking anti-patterns) is in test-code-conventions (qa-test-review plugin).
When to use
Step 1 - Install
pip install pytest
# Common plugins:
pip install pytest-cov pytest-asyncio pytest-mock pytest-xdistStep 2 - First test
# test_sum.py
def sum(a, b):
return a + b
def test_adds_1_and_2():
assert sum(1, 2) == 3pytestpytest auto-discovers via test_*.py / *_test.py filenames and test_* / Test* function/class names.
Step 3 - Configuration
pytest.ini (or pyproject.toml [tool.pytest.ini_options] / setup.cfg [tool:pytest]):
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-ra --strict-markers --strict-config"
markers = [
"slow: marks tests as slow (deselect with -m 'not slow')",
"integration: marks tests requiring DB/external resources",
]--strict-markers rejects undeclared marker names - catches typos like @pytest.mark.skipp (silently skipped before).
Step 4 - Fixtures
import pytest
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session")
def app_config():
return load_config()
@pytest.fixture(autouse=True)
def reset_state():
yield
cleanup_after_test()
def test_user_creation(db_connection, app_config):
user = create_user(db_connection, app_config)
assert user.id is not NoneFixture scopes: function (default), class, module, package, session. Choose narrowest scope that doesn't waste setup time.
conftest.py shares fixtures across multiple test files in the same directory (and subdirectories).
Step 5 - Parametrize
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_sum_parametrized(a, b, expected):
assert sum(a, b) == expectedMulti-param multiply (cross-product):
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", ['a', 'b'])
def test_combinations(x, y):
# runs 6 times: (1,'a'), (1,'b'), (2,'a'), ...
passStep 6 - Markers + skip/xfail
@pytest.mark.skip(reason="Requires staging DB")
def test_skip_example():
pass
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Python 3.11+ syntax")
def test_modern_syntax():
pass
@pytest.mark.xfail(reason="Known bug; tracked in JIRA-1234")
def test_known_failure():
assert 1 == 2
@pytest.mark.slow
def test_long_running():
passFilter: pytest -m "not slow" skips slow-marked tests.
Step 7 - Mocking with pytest-mock
def test_with_mock(mocker):
mock_api = mocker.patch('mymodule.api_client.fetch')
mock_api.return_value = {'id': 1, 'name': 'Alice'}
result = my_function()
mock_api.assert_called_once_with('/users')
assert result == {'id': 1, 'name': 'Alice'}mocker fixture from pytest-mock wraps unittest.mock.patch with auto-cleanup at test end.
Step 8 - Async (pytest-asyncio)
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_function():
result = await fetch_data()
assert result == 'expected'
# Or set asyncio_mode = "auto" in pyproject.toml to skip the markerStep 9 - Coverage with pytest-cov
pytest --cov=src --cov-report=term-missing --cov-report=html --cov-report=xml \
--cov-fail-under=80--cov-fail-under=N fails the run if coverage drops below N%.
Full [tool.coverage.*] config, CI wiring, and parallel execution: references/coverage-and-ci.md.
Step 10 - Fast-feedback flags
pytest --lf # only re-run last-failed tests
pytest --ff # run last-failed first, then the rest
pytest -x # stop on first failure
pytest -k "name_pat" # only tests matching name pattern
pytest -v # verbose
pytest -s # don't capture stdout (see print() output)
pytest -p no:cacheprovider # disable test-cache (CI cache-clean runs)Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Use setUp / tearDown (TestCase style) instead of fixtures | Loses dependency injection benefits | Use fixtures (Step 4) |
Skip --strict-markers | Typos in markers silently skip tests | Always set in config (Step 3) |
Fixture with scope='session' for stateful resources | State leaks across tests | Function-scope unless setup expensive |
pytest -k 'expr' in CI to skip "slow" tests | Brittle string match | Use -m markers (Step 6) |
Skip --cov-fail-under in CI | Coverage drops silently over time | Always gate coverage (Step 9) |
Limitations
References
pytest coverage config and CI integration
View source (opens in new window)pytest coverage config and CI integration
Deeper coverage configuration and CI wiring for pytest. The SKILL.md spine keeps the minimal pytest --cov ... --cov-fail-under=80 command; the exhaustive config and pipeline wiring live here.
Coverage config (pyproject.toml)
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["**/__init__.py", "**/types.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
fail_under = 80branch = true enables branch coverage (not just line coverage). exclude_lines drops lines that can never meaningfully be covered from the denominator. fail_under mirrors the --cov-fail-under flag as a config default.
CI integration (GitHub Actions)
- run: pip install -e .[dev]
- run: pytest --cov --cov-report=xml --cov-fail-under=80 --junitxml=junit.xml
- uses: codecov/codecov-action@v4
with: { files: coverage.xml }--junitxml=junit.xml emits a JUnit report for CI test-result annotations; --cov-report=xml emits coverage.xml for the coverage uploader.
Parallel execution (pytest-xdist)
pytest -n auto # uses CPU countpytest-xdist distributes tests across worker processes. Combine with coverage via --cov (pytest-cov merges per-worker data automatically).
Related skills
doctest-tests
Configures and runs Python's stdlib doctest - embeds executable test cases in docstrings using `>>>` Python interactive prompt convention; supports `# doctest: +ELLIPSIS` / `+NORMALIZE_WHITESPACE` / `+SKIP` directives; integrates with pytest via `--doctest-modules` flag; runs as `python -m doctest module.py -v`. Use for self-documenting reference implementations + simple smoke-test coverage embedded in API docs.
nose2-tests
Configures and runs nose2 - successor to nose (the original Python test discovery library, end-of-life 2015) and an alternative to pytest's discovery model; supports plugin architecture, layers (per-test-class setUp/tearDown shared across modules), parameterized tests via `nose2.tools.params`, multi-process parallelism via mp plugin. Use when migrating from legacy nose1 codebases or when the team prefers nose2's plugin model over pytest.
pytest-asyncio-patterns
Configures and runs async Python tests with pytest-asyncio: installs the plugin, selects asyncio_mode (auto vs strict), scopes event loops (function/class/module/session), writes async fixtures with @pytest_asyncio.fixture, mocks coroutines with AsyncMock, and tests FastAPI (httpx.AsyncClient + ASGITransport) and aiohttp (aiohttp_client fixture) applications. Use when a Python project contains async def test_ functions, FastAPI/aiohttp endpoints, or any asyncio-based code that needs pytest integration. Do NOT use for general pytest fixture design, parametrize patterns, or conftest.py structure without an asyncio-specific problem (event-loop scoping, mode config, AsyncMock, ASGI client): use pytest-tests for those.
unittest-tests
Configures and runs Python's stdlib unittest - TestCase + setUp/tearDown lifecycle hooks, assertion catalog (assertEqual / assertRaises / assertIn / assertAlmostEqual), unittest.mock module (Mock / MagicMock / patch / patch.object / patch.dict), test discovery via `python -m unittest discover`, subTest for parametrized cases, expectedFailure decorator. Use when constrained to stdlib-only (no pip install) or migrating legacy unittest codebases.