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.

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.0.tar.gz (22.7 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.0-py3-none-any.whl (24.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pytest_drill_sergeant-0.3.0.tar.gz
  • Upload date:
  • Size: 22.7 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.0.tar.gz
Algorithm Hash digest
SHA256 f39e1805c9600f3c432c44fdac0910df5a34ad413d0332890ab708d358004bc1
MD5 9d951de7477653f07ea5ecbf163f9296
BLAKE2b-256 3958b4f21c0fc8c45eb488645e4ef89152b9b0cab5ac781fb675dd48f0838afe

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for pytest_drill_sergeant-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ee27966f8f94a169422067c2901bf5387711093581f2abf24e7707f17055bf10
MD5 3c174039880275eafded69c358bd0e86
BLAKE2b-256 ed9575edbe0bb6ecaa827f561be77a34eb79673f6bdc5622ce939d965f721921

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