Testland
Browse all skills & agents

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-tests
View source

pytest-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

  • Modern Python project; pytest is the default for new code.
  • Migrating from unittest (mostly mechanical via interop).
  • Need fixture-based dependency injection + parametrize.

Step 1 - Install

pip install pytest
# Common plugins:
pip install pytest-cov pytest-asyncio pytest-mock pytest-xdist

Step 2 - First test

# test_sum.py
def sum(a, b):
    return a + b

def test_adds_1_and_2():
    assert sum(1, 2) == 3
pytest

pytest 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 None

Fixture 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) == expected

Multi-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'), ...
    pass

Step 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():
    pass

Filter: 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 marker

Step 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-patternWhy it failsFix
Use setUp / tearDown (TestCase style) instead of fixturesLoses dependency injection benefitsUse fixtures (Step 4)
Skip --strict-markersTypos in markers silently skip testsAlways set in config (Step 3)
Fixture with scope='session' for stateful resourcesState leaks across testsFunction-scope unless setup expensive
pytest -k 'expr' in CI to skip "slow" testsBrittle string matchUse -m markers (Step 6)
Skip --cov-fail-under in CICoverage drops silently over timeAlways gate coverage (Step 9)

Limitations

  • Plugin ecosystem is large; conflicting plugins can cause subtle issues.
  • Fixture-scope reasoning has learning curve.
  • assert rewriting requires pytest's importer; some patterns (running tests as scripts) bypass it.
  • Async support requires pytest-asyncio; built-in support is limited.

References

  • pt-docs (opens in new window) - official documentation
  • pytest.org - landing
  • pypi.org/project/pytest-mock - pytest-mock plugin
  • pypi.org/project/pytest-asyncio - async support
  • pypi.org/project/pytest-xdist - parallel execution
  • unittest-tests, doctest-tests, nose2-tests - sister tools
  • test-code-conventions - test code hygiene

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 = 80

branch = 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 count

pytest-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.