A pytest plugin that enforces test quality standards through automatic marker detection and AAA structure validation
Project description
Pytest Drill Sergeant
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.iniandpyproject.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 requiredstrict: 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: failwarn: report onlyoff: 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):
- environment variables
- pytest config (
pytest.inior[tool.pytest.ini_options]) [tool.drill_sergeant]inpyproject.toml- 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_ENABLEDDRILL_SERGEANT_ENFORCE_MARKERSDRILL_SERGEANT_ENFORCE_AAADRILL_SERGEANT_MARKER_SEVERITY(error|warn|off)DRILL_SERGEANT_AAA_SEVERITY(error|warn|off)DRILL_SERGEANT_AAA_MODEDRILL_SERGEANT_ENFORCE_FILE_LENGTHDRILL_SERGEANT_FILE_LENGTH_MODEDRILL_SERGEANT_FILE_LENGTH_EXCLUDEDRILL_SERGEANT_FILE_LENGTH_INLINE_IGNOREDRILL_SERGEANT_FILE_LENGTH_INLINE_IGNORE_TOKENDRILL_SERGEANT_AUTO_DETECT_MARKERSDRILL_SERGEANT_WRITE_MARKERS(write auto-detected markers into source files)DRILL_SERGEANT_MIN_DESCRIPTION_LENGTHDRILL_SERGEANT_MAX_FILE_LENGTHDRILL_SERGEANT_MARKER_MAPPINGSDRILL_SERGEANT_DEBUG_CONFIGDRILL_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 -quv run ruff check src testsuv 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:
- Conventional commits land on
main. Release Pleaseauto-opens/updates release PRs.- Review and merge the generated release PR.
- Confirm GitHub Release + tag were created.
- Run
Production Release (PyPI)workflow manually withrelease_tag.
Operational rule:
- do not hand-edit versions or changelog outside the release PR
- keep
release-pleaseautomated 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.mdfor failure-to-fix mappingdocs/Decision-Log.mdfor scope decisions and rationaledocs/Release-Checklist.mdfor release executionSTABILIZATION_PLAN.mdfor 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e16c29d06650f39ea40b510dcdbcf5955990e267356f1f2becb8c8a9e86920b
|
|
| MD5 |
8af321896d11817504c93b1d2683a637
|
|
| BLAKE2b-256 |
d81a14b52124ca26fd66bb53e739c902b4c55e100683701a45ffd7e627795c62
|
File details
Details for the file pytest_drill_sergeant-0.4.0-py3-none-any.whl.
File metadata
- Download URL: pytest_drill_sergeant-0.4.0-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48ec86658fd0ad74924728f4de8aa11c416c2ebf3e7f75daa5a81560b06653d6
|
|
| MD5 |
afa4d30053ff11189e9c20269d09cb06
|
|
| BLAKE2b-256 |
f35d7c9ebdc0c53a5c68dea19a747936cc046bd1465742f0d58973f6df73f50b
|