Skip to main content

A pytest plugin that enforces test quality standards through automatic marker detection and AAA structure validation

Project description

Pytest Drill Sergeant

CI Status codecov PyPI version

You want elite tests? Then stop writing lazy chaos and start writing disciplined test code.

This plugin is your no-excuses drill instructor for:

  • marker classification
  • AAA structure (Arrange / Act / Assert)
  • file-length discipline

It does not care about feelings. It cares about standards.

Mission Profile

1. Marker Rule

What it does:

  • validates useful test markers
  • auto-detects marker intent from path (for example tests/unit/ -> @pytest.mark.unit)
  • can write auto-detected markers into source files (drill_sergeant_write_markers = true, default)
  • supports custom directory-to-marker mappings
  • reads marker declarations from both pytest.ini and pyproject.toml ([tool.pytest.ini_options])

What you do:

  • put tests in intentional directories
  • keep marker declarations real
  • stop shipping unclassified tests

2. AAA Rule

What it does:

  • enforces explicit AAA section comments in test bodies
  • supports two modes:
    • basic: section presence required
    • strict: presence + order + no duplicate section declarations
  • supports built-in/custom synonyms when enabled

Accepted grammar:

# <Keyword> - <description>

Examples:

# Arrange - create test fixture
# Act - call the function
# Assert - verify expected behavior

3. File-Length Rule

What it does:

  • enforces max test file length
  • supports modes:
    • error: fail
    • warn: report only
    • off: disabled
  • supports path exclusions and inline ignore token

Inline ignore example:

# drill-sergeant: file-length ignore

Use this sparingly. If you need it everywhere, the file should be split.

Quick Start

Install

uv add --group dev pytest-drill-sergeant

Minimal pytest.ini

[pytest]
addopts = -p drill_sergeant
markers =
    unit: Unit tests
    integration: Integration tests
    e2e: End-to-end tests

drill_sergeant_enabled = true
drill_sergeant_enforce_markers = true
drill_sergeant_enforce_aaa = true
drill_sergeant_enforce_file_length = true
drill_sergeant_marker_severity = error
drill_sergeant_aaa_severity = error
drill_sergeant_aaa_mode = basic
drill_sergeant_auto_detect_markers = true
drill_sergeant_max_file_length = 350

Minimal Passing Test

import pytest


@pytest.mark.unit
def test_addition() -> None:
    # Arrange - prepare operands
    left = 2
    right = 3

    # Act - run operation
    total = left + right

    # Assert - validate result
    assert total == 5

Running by marker

With markers (and optional drill_sergeant_auto_detect_markers = true), use pytest’s -m to select tests:

  • Only unit tests:
    pytest -m unit
  • Everything except e2e:
    pytest -m "not e2e"
  • Unit or integration:
    pytest -m "unit or integration"

Register every marker you use in [pytest] markers (as in the minimal config above) so pytest accepts -m and doesn’t warn about unknown markers.

Configuration

Precedence (highest to lowest):

  1. environment variables
  2. pytest config (pytest.ini or [tool.pytest.ini_options])
  3. [tool.drill_sergeant] in pyproject.toml
  4. plugin defaults

pyproject.toml Example

[tool.drill_sergeant]
enabled = true
enforce_markers = true
enforce_aaa = true
aaa_mode = "basic"
marker_severity = "error"
aaa_severity = "error"
enforce_file_length = true
file_length_mode = "error"
file_length_exclude = ["tests/legacy/*"]
file_length_inline_ignore = true
file_length_inline_ignore_token = "drill-sergeant: file-length ignore"
auto_detect_markers = true
write_markers = true
min_description_length = 3
max_file_length = 350
aaa_synonyms_enabled = false
aaa_builtin_synonyms = true

[tool.drill_sergeant.marker_mappings]
contract = "api"
smoke = "integration"

Environment Variables

  • DRILL_SERGEANT_ENABLED
  • DRILL_SERGEANT_ENFORCE_MARKERS
  • DRILL_SERGEANT_ENFORCE_AAA
  • DRILL_SERGEANT_MARKER_SEVERITY (error | warn | off)
  • DRILL_SERGEANT_AAA_SEVERITY (error | warn | off)
  • DRILL_SERGEANT_AAA_MODE
  • DRILL_SERGEANT_ENFORCE_FILE_LENGTH
  • DRILL_SERGEANT_FILE_LENGTH_MODE
  • DRILL_SERGEANT_FILE_LENGTH_EXCLUDE
  • DRILL_SERGEANT_FILE_LENGTH_INLINE_IGNORE
  • DRILL_SERGEANT_FILE_LENGTH_INLINE_IGNORE_TOKEN
  • DRILL_SERGEANT_AUTO_DETECT_MARKERS
  • DRILL_SERGEANT_WRITE_MARKERS (write auto-detected markers into source files)
  • DRILL_SERGEANT_MIN_DESCRIPTION_LENGTH
  • DRILL_SERGEANT_MAX_FILE_LENGTH
  • DRILL_SERGEANT_MARKER_MAPPINGS
  • DRILL_SERGEANT_DEBUG_CONFIG
  • DRILL_SERGEANT_DEBUG_TELEMETRY (prints per-validator timing summary at session end)

Return Type Policy

Return-type annotation enforcement is intentionally handled by static tooling, not runtime test hooks.

Use Ruff ANN rules:

uv run ruff check --fix src tests

CI Contract

Required gates:

  • uv run pytest -q
  • uv run ruff check src tests
  • uv run mypy src tests --config-file=pyproject.toml

Local parity command:

just verify

Coverage is informational, not a required merge gate.

Release Workflow (Current)

Release flow is split by responsibility:

  1. Conventional commits land on main.
  2. Release Please auto-opens/updates release PRs.
  3. Review and merge the generated release PR.
  4. Confirm GitHub Release + tag were created.
  5. Run Production Release (PyPI) workflow manually with release_tag.

Operational rule:

  • do not hand-edit versions or changelog outside the release PR
  • keep release-please automated for version/changelog hygiene
  • do not auto-publish to PyPI from release events
  • keep release notes derived from conventional commits

Failure Intel

If a rule fails, use:

  • docs/Failure-Catalog.md for failure-to-fix mapping
  • docs/Decision-Log.md for scope decisions and rationale
  • docs/Release-Checklist.md for release execution
  • STABILIZATION_PLAN.md for phased recovery status

Development

Common commands:

just verify
just test
just lint
just type-check

Release Flow

Use docs/Release-Checklist.md as the canonical release runbook.

Final Word

The point is not ceremony. The point is predictable, readable, maintainable test code under pressure.

Do the basics with discipline and your test suite will stop betraying you.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_drill_sergeant-0.4.0.tar.gz (25.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_drill_sergeant-0.4.0-py3-none-any.whl (26.5 kB view details)

Uploaded Python 3

File details

Details for the file pytest_drill_sergeant-0.4.0.tar.gz.

File metadata

  • Download URL: pytest_drill_sergeant-0.4.0.tar.gz
  • Upload date:
  • Size: 25.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytest_drill_sergeant-0.4.0.tar.gz
Algorithm Hash digest
SHA256 4e16c29d06650f39ea40b510dcdbcf5955990e267356f1f2becb8c8a9e86920b
MD5 8af321896d11817504c93b1d2683a637
BLAKE2b-256 d81a14b52124ca26fd66bb53e739c902b4c55e100683701a45ffd7e627795c62

See more details on using hashes here.

File details

Details for the file pytest_drill_sergeant-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_drill_sergeant-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 48ec86658fd0ad74924728f4de8aa11c416c2ebf3e7f75daa5a81560b06653d6
MD5 afa4d30053ff11189e9c20269d09cb06
BLAKE2b-256 f35d7c9ebdc0c53a5c68dea19a747936cc046bd1465742f0d58973f6df73f50b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page