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

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

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
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_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 release-please + GitHub Releases:

  1. Conventional commits land on main.
  2. release-please opens/updates a release PR with version + changelog.
  3. Merge the release PR.
  4. GitHub Release + tag are created automatically.
  5. .github/workflows/release.yml publishes to PyPI on release.published.

Operational rule:

  • do not hand-edit versions or changelog outside the release PR
  • do not publish manually unless recovery is explicitly needed
  • 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

Releases are managed by release-please:

  1. Push conventional commits to main (feat:, fix:, etc.).
  2. Release Please workflow opens/updates a release PR.
  3. Merge the release PR to create a GitHub Release + tag.
  4. Production Release (PyPI) publishes to PyPI on release publish event.

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.3.1.tar.gz (23.1 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.3.1-py3-none-any.whl (24.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pytest_drill_sergeant-0.3.1.tar.gz
  • Upload date:
  • Size: 23.1 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.3.1.tar.gz
Algorithm Hash digest
SHA256 21b335dd4c1dd3e4bddfb0e5aaa0940edf81e0fdbfaab8c326a7dd5f7fad7230
MD5 b3899b3a556cad8a6324762ca1aef236
BLAKE2b-256 5473f64a84d83f33847b8fb05e0d7ffd157efa60703256eae12ecee9facbcd94

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for pytest_drill_sergeant-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a3a370ffed0cb62a4ca78f869f50322e7a6cd3bb9c6dfd41eb7d80c54fd85407
MD5 408ae4b05c57606f33fdc80a7385a959
BLAKE2b-256 add91ea33bdfeab530151c1665be30673376561f6309d5fd6ae41b15c79a1044

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