CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/python-unit-tests

Use whenever the task is to write, add, fix or review Python tests, even quick pytest tests for a single function: edge cases, fixtures and conftest.py, parametrize tables, mocking, markers, coverage gates and CI. Has the pytest checklist for edge cases and weak assertions.

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
python-unit-tests
description:
Use whenever the task is to write, add, fix or review Python tests, even quick pytest tests for a single function: edge cases, fixtures and conftest.py, parametrize tables, mocking, markers, coverage gates and CI. Has the pytest checklist for edge cases and weak assertions.

python-unit-tests

Overview

Per docs.pytest.org/en/stable:

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

Choosing a framework

  1. pytest for new code - the modern default; migration from unittest is mostly mechanical because pytest runs TestCase classes natively.
  2. Match an existing unittest convention when maintaining a legacy suite or constrained to stdlib-only (no pip install) → references/unittest.md. unittest.mock is the canonical mocking library in either style.
  3. doctest only for documentation-as-tests - executable examples in docstrings, not regression coverage → references/doctest.md.
  4. Async code → the standalone pytest-asyncio-patterns skill (loop scoping, modes, AsyncMock).

Step 1 - Install

pip install "pytest>=9"
# 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 (pt-docs).

Step 3 - Configuration

pytest 9 (2025-11-05) reads native TOML. Per pt-config: "Use [tool.pytest] to leverage native TOML types (supported since pytest 9.0)". New projects on pytest 9+ use that table:

# pyproject.toml (pytest 9+)
[tool.pytest]
minversion = "9.0"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
addopts = ["-ra"]
strict = true
markers = [
    "slow: marks tests as slow (deselect with -m 'not slow')",
    "integration: marks tests requiring DB/external resources",
]

Per the changelog, strict = true enables strict_config, strict_markers, strict_parametrization_ids and strict_xfail in one switch, and future strictness options join it. Strict markers reject undeclared names, which catches typos like @pytest.mark.skipp (silently skipped before).

Other config locations, and what to keep when a project already has one:

File and tablepytest versionNotes
pyproject.toml [tool.pytest]9.0+Native TOML lists, booleans and numbers.
pyproject.toml [tool.pytest.ini_options]6.0+INI-style string values (addopts = "-ra -q"). Per the changelog it "can't be used alongside [tool.pytest]": convert, don't add a second table.
pytest.toml or .pytest.toml [pytest]9.0+Per pt-config, "pytest.toml files take precedence over other files, even when empty."
pytest.ini, tox.ini [pytest], setup.cfg [tool:pytest]allOlder INI formats; still read.

Per pt-config, "Options from multiple configfiles candidates are never merged - the first match wins", so a stray pytest.ini silently hides the pyproject.toml table. Check which file pytest picked from the configfile: line in the run header.

On pytest 8 or older, keep [tool.pytest.ini_options] with addopts = "-ra --strict-markers --strict-config"; the strict switch does not exist there.

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 the narrowest scope that doesn't waste setup time. conftest.py shares fixtures across test files in the same directory (and subdirectories). Fixtures are requested by naming them as test-function parameters - pytest "searches for fixtures that have the same names as those parameters" (docs.pytest.org/how-to/fixtures).

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

Stacked @pytest.mark.parametrize decorators multiply into a cross-product (docs.pytest.org/how-to/parametrize).

Step 6 - Markers + skip/xfail

@pytest.mark.skip(reason="Requires staging DB")
def test_skip_example(): ...

@pytest.mark.skipif(sys.version_info < (3, 11), reason="Python 3.11+ syntax")
def test_modern_syntax(): ...

@pytest.mark.xfail(reason="Known bug; tracked in JIRA-1234")
def test_known_failure():
    assert 1 == 2

@pytest.mark.slow
def test_long_running(): ...

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'}

The mocker fixture from pytest-mock wraps unittest.mock.patch with auto-cleanup at test end. Patch target rule: patch where the name is used, not where it's defined - if mymodule.py does from api import fetch_user, patch mymodule.fetch_user, not api.fetch_user (see references/unittest.md for the full unittest.mock catalog).

Step 8 - 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%. Config-side equivalent in 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 (not just line) coverage; exclude_lines drops never-coverable lines from the denominator.

Step 9 - CI and parallel execution

- run: pip install -e .[dev]
- run: pytest --cov --cov-report=xml --cov-fail-under=80 --junitxml=junit.xml
- uses: codecov/codecov-action@v7
  with: { files: coverage.xml }

--junitxml=junit.xml emits a JUnit report (feeds junit-xml-analysis in qa-test-reporting); --cov-report=xml emits coverage.xml for the uploader. Parallel: pytest -n auto via pytest-xdist distributes tests across workers; pytest-cov merges per-worker data automatically.

Step 10 - Subtests for one test over many items

pytest 9 adds a subtests fixture. Per the changelog, its subtests.test() context manager "reports each failing subtest separately", and the feature is marked experimental. Use it when one test checks many items and every failure should show, not only the first:

def test_every_price_rounds_to_cents(subtests, catalog):
    for item in catalog:
        with subtests.test(msg=item.sku):
            assert round(item.price, 2) == item.price

Prefer @pytest.mark.parametrize when the inputs are known at collection time; subtests fit data produced at run time.

Step 11 - 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 -s              # don't capture stdout (see print() output)

Authoring conventions

When authoring a new unit test in an existing project:

  1. Detect the framework, never assume. Check pytest.toml, pyproject.toml ([tool.pytest], [tool.pytest.ini_options] or pytest in dev-deps), setup.cfg ([tool:pytest]), tox.ini ([pytest]); then grep existing tests - unittest.TestCase subclasses → unittest; otherwise default to pytest. Doctest is opt-in per-module, only when the spec explicitly asks for in-docstring examples. Conflicting signals → stop and ask.
  2. Match the layout. Existing tests/ dir → tests/test_<module>.py; co-located test_<module>.py → match it. For doctest, patch the source module's docstring instead of creating a file.
  3. One spec → one new test; never modify existing test methods and never fabricate attributes/methods the target module does not expose.
  4. Assert the spec's concrete outcome - no assert True / self.assertTrue(True) smoke asserts. Plain assert in pytest functions; self.assertEqual in TestCase classes (diff-aware). Cover the edges, not only the happy path: empty and whitespace input, the smallest and largest valid values, one step past each boundary, and malformed input. Assert each documented error with pytest.raises(ValueError, match=...) (pytest: expected exceptions); for systematic boundaries generate the cases with boundary-value-generator (qa-test-data).
  5. Use present data peers only: mimesis in dev-deps → locale-aware fixtures via synthetic-data-toolkit (qa-test-data); never install packages as a side effect. 3+ interacting inputs → generate the case set with pairwise-test-case-generator (qa-test-data), then map through @pytest.mark.parametrize.
  6. Refuse universally-quantified specs ("holds for all valid inputs") - that is property-based-test scope (qa-property-based plugin).
  7. Beware mixed lifecycles: pytest's setup_method runs alongside (not instead of) setUp on TestCase subclasses - pick one mechanism per class. Avoid mutable default arguments in test helpers (shared across calls; leaks state).

Anti-patterns

Anti-patternWhy it failsFix
setUp / tearDown (TestCase style) in new pytest codeLoses dependency-injection benefitsFixtures (Step 4)
Skip strict mode (strict = true on pytest 9, --strict-markers before)Marker typos silently skip testsAlways set in config (Step 3)
Both [tool.pytest] and [tool.pytest.ini_options] in one pyproject.tomlpytest 9 refuses the combinationConvert to one table (Step 3)
A leftover pytest.ini next to a pyproject.toml tableFirst config file wins; the other is ignored without warningKeep one config file (Step 3)
Passing ids, indirect or scope positionally to parametrizeRaises TypeError since pytest 9.2 (changelog)Pass them as keywords
scope='session' fixtures for stateful resourcesState leaks across testsFunction scope unless setup is expensive
pytest -k 'expr' in CI to skip slow testsBrittle string match-m markers (Step 6)
Skip --cov-fail-under in CICoverage drops silently over timeAlways gate coverage (Step 8)
doctest as the only test surface for non-trivial logicBrittle string matching; no fixtures/mockspytest for regression coverage; doctest for examples

Limitations

  • Plugin ecosystem is large; conflicting plugins can cause subtle issues.
  • Fixture-scope reasoning has a learning curve.
  • assert rewriting requires pytest's importer; running tests as scripts bypasses it.
  • Built-in async support is limited - use pytest-asyncio (see pytest-asyncio-patterns).

References

  • pt-docs - official pytest documentation
  • pt-config - config files: [tool.pytest], pytest.toml, precedence, first match wins
  • pt-changelog - pytest 9.0 native TOML, strict, subtests; 9.2 keyword-only parametrize arguments
  • docs.pytest.org/en/stable/how-to/fixtures.html - fixtures
  • docs.pytest.org/en/stable/how-to/parametrize.html - parametrize
  • pt-raises - pytest.raises and match for expected exceptions
  • pypi.org/project/pytest-mock - pytest-mock plugin
  • pypi.org/project/pytest-xdist - parallel execution
  • references/unittest.md - stdlib unittest + unittest.mock
  • references/doctest.md - stdlib doctest
  • pytest-asyncio-patterns - async test patterns (standalone sibling)
  • test-code-conventions (qa-test-review) - test code hygiene
Workspace
testland
Visibility
Public
Created
Last updated
Source
GitHub Actions
Repository
https://github.com/testland/qaVerified
Badge
testland/python-unit-tests badge