A pytest plugin that reviews the quality of your tests
Project description
pytest-review
A pytest plugin that reviews the quality of your tests.
Overview
pytest-review analyzes your test suite and provides actionable feedback on test quality. It detects common anti-patterns, missing assertions, overly complex tests, and more.
Features
- Static Analysis: AST-based detection of test quality issues
- Dynamic Analysis: Runtime performance tracking
- Multiple Output Formats: Terminal, JSON, and HTML reports
- Configurable: Customize thresholds and enable/disable analyzers
- Quality Scoring: Get a letter grade (A-F) for your test suite
- Incremental Caching: Skip re-analysis of unchanged files across runs
- Parallel Analysis: Distribute static analysis across multiple processes
- Plugin API: Register custom analyzers via entry points
Analyzers
| Analyzer | Description |
|---|---|
| assertions | Detects empty tests, trivial assertions (assert True), tautologies |
| naming | Checks for descriptive test names, proper snake_case |
| complexity | Flags tests with too many statements, deep nesting, high cyclomatic complexity |
| patterns | Identifies anti-patterns: bare except, time.sleep, print statements |
| isolation | Detects global state modifications, class attribute mutations |
| performance | Tracks slow tests at runtime |
| smells | Detects test smells: assertion roulette, duplicate asserts, eager tests, magic numbers |
Installation
pip install pytest-review
Quick Start
Run pytest with the --review flag:
pytest --review
Example output:
====================== pytest-review: Test Quality Report ======================
[X] <assertions> tests/test_example.py:15 [test_empty] Test has no assertions
Suggestion: Add at least one assertion to verify expected behavior
[!] <complexity> tests/test_example.py:20 [test_complex] Test has cyclomatic complexity of 12
Suggestion: Simplify test logic or split into multiple tests
----------------------------------- Summary ------------------------------------
Tests analyzed: 25
Errors: 2
Warnings: 5
Quality: NEEDS IMPROVEMENT
Overall Score: 72.0/100 (C)
================================================================================
By default, info-level suggestions are hidden. Pass --review-min-severity=info to see them.
Command Line Options
| Option | Description |
|---|---|
--review |
Enable test quality review |
--review-format |
Output format: terminal (default), json, html |
--review-output |
Write report to file |
--review-strict |
Fail if quality errors are found |
--review-min-score |
Minimum required score (0-100) |
--review-min-severity |
Only show issues at or above this severity: info, warning (default), error. Display only -- does not affect scoring or --review-strict. |
--review-only |
Comma-separated list of analyzers to run |
--review-exclude |
Comma-separated list of analyzers to exclude |
--review-diff |
Only analyze tests in files changed relative to a base branch (default: auto-detect main/master) |
--review-workers |
Number of parallel worker processes for static analysis. 0 = auto (default), 1 = sequential |
--review-no-cache |
Disable incremental result caching across runs |
Examples
# Generate HTML report
pytest --review --review-format=html --review-output=report.html
# Generate JSON report
pytest --review --review-format=json --review-output=report.json
# Run only specific analyzers
pytest --review --review-only=assertions,naming
# Fail CI if score below 80
pytest --review --review-min-score=80
# Strict mode: fail on any errors
pytest --review --review-strict
# Show errors only (hide warnings and info)
pytest --review --review-min-severity=error
# Show everything, including info-level suggestions
pytest --review --review-min-severity=info
# Force sequential analysis (disable parallelism)
pytest --review --review-workers=1
# Disable result caching
pytest --review --review-no-cache
Configuration
Configure pytest-review in your pyproject.toml:
[tool.pytest-review]
enabled = true
strict = false
min_score = 0
min_severity = "warning" # display threshold: info, warning, or error
[tool.pytest-review.analyzers]
assertions = { enabled = true, min_assertions = 1 }
naming = { enabled = true, min_length = 10 }
complexity = { enabled = true, max_statements = 20, max_depth = 3, max_complexity = 5 }
patterns = { enabled = true }
isolation = { enabled = true }
performance = { enabled = true, slow_threshold_ms = 500, very_slow_threshold_ms = 2000 }
smells = { enabled = true, max_assertions_without_message = 1, check_magic_numbers = true }
Skipping Tests
Use the review_skip marker to exclude specific tests from review:
import pytest
@pytest.mark.review_skip
def test_intentionally_complex():
# This test won't be analyzed
...
Scoring System
The quality score is calculated using weighted categories:
| Category | Weight | Analyzers |
|---|---|---|
| Assertions | 30% | assertions |
| Clarity | 25% | naming, smells |
| Isolation | 20% | isolation |
| Simplicity | 15% | complexity, patterns |
| Performance | 10% | performance |
Severity penalties:
- Error: -15 points per issue
- Warning: -5 points per issue
- Info: -1 point per issue
Critical penalties (applied globally):
- Missing assertions: -20 points
- Trivial assertions: -10 points
Grade Scale
| Grade | Score Range |
|---|---|
| A | 90-100 |
| B | 80-89 |
| C | 70-79 |
| D | 60-69 |
| F | 0-59 |
Issue Types
Errors (X)
Critical issues that indicate likely bugs or useless tests:
assertions.missing- Test has no assertionsassertions.trivial- Trivial assertion likeassert Trueassertions.tautology- Comparing value to itselfsmells.swallowed_assertion-except AssertionError/Exception/BaseExceptionsilently swallows assertion failures
Warnings (!)
Issues that may indicate problems:
naming.non_descriptive- Generic names liketest_foocomplexity.too_many_statements- Test too longcomplexity.too_deep- Excessive nestingcomplexity.too_complex- High cyclomatic complexitypatterns.bare_except- Catches all exceptionspatterns.sleep_in_test- Usestime.sleep()isolation.global_modification- Modifies global stateisolation.process_mutation- Mutates process-wide state (os.chdir,sys.path,sys.argv)smells.assertion_roulette- Multiple assertions without messagessmells.duplicate_assert- Duplicate assertion statementssmells.ignored_test- Test is skipped via decorator,pytest.skip(...),pytest.xfail(...),self.skipTest(...), orraise SkipTest(...)smells.early_return-returnin a test body, bypassing subsequent assertions
Info (i)
Suggestions for improvement. Hidden by default -- run with --review-min-severity=info (or set min_severity = "info" in pyproject.toml) to see them:
naming.too_short- Name could be more descriptivepatterns.print_statement- Debug print left in testperformance.slow_test- Test runs slowlysmells.magic_number- Literal number in assertionsmells.eager_test- Test verifies multiple methodsassertions.low_value- Weak assertion (isinstance,is not None)assertions.yoda_condition- Reversed comparison (assert 42 == x)assertions.raises_without_match-pytest.raises()withoutmatch=
Performance
Incremental Caching
Static analysis results are cached per file, keyed on a SHA-256 content hash and a hash of the active analyzer configuration. On subsequent runs, unchanged files are skipped entirely. The cache is stored in pytest's .pytest_cache/ directory and is invalidated automatically when file contents or config change.
Disable caching with --review-no-cache. Clear the cache with pytest's built-in --cache-clear.
Parallel Analysis
For large test suites, static analysis can run in parallel across files using a ProcessPoolExecutor. By default (--review-workers=0), parallelism is auto-enabled when the suite has 200+ tests across 8+ files. Use --review-workers=N to set a specific worker count, or --review-workers=1 to force sequential execution.
Custom Analyzers
Third-party packages can register custom analyzers via the pytest_review entry point group. No changes to pytest-review are required.
Creating an Analyzer
Subclass StaticAnalyzer (for AST-based analysis) or DynamicAnalyzer (for runtime analysis):
# my_package/analyzer.py
from pytest_review.analyzers.base import (
AnalyzerResult, Issue, Severity, StaticAnalyzer, TestItemInfo,
)
class MyAnalyzer(StaticAnalyzer):
name = "my-analyzer"
description = "Checks for my custom pattern"
category = "clarity" # scoring category (see below)
def _analyze_ast(self, test: TestItemInfo, result: AnalyzerResult) -> None:
# Walk test.node (an ast.FunctionDef) and add issues
result.add_issue(
Issue(
rule="my-analyzer.example",
message="Example issue found",
severity=Severity.WARNING,
file_path=test.file_path,
line=test.line,
test_name=test.name,
suggestion="How to fix it",
)
)
Registering via Entry Points
In your package's pyproject.toml, declare the entry point:
[project.entry-points.pytest_review]
my-analyzer = "my_package.analyzer:MyAnalyzer"
Once the package is installed, pytest-review discovers the analyzer automatically when --review is used.
Configuration
Users configure custom analyzers the same way as built-in ones:
[tool.pytest-review.analyzers.my-analyzer]
enabled = true
custom_option = 42
Options are accessible in the analyzer via self.get_option("custom_option", default=0).
Scoring Integration
Set the category class attribute to one of the 5 scoring categories so issues contribute to the quality score:
| Category | Weight |
|---|---|
assertions |
30% |
clarity |
25% |
isolation |
20% |
simplicity |
15% |
performance |
10% |
Analyzers without a category are still reported but do not affect the score.
Filtering
Custom analyzers work with --review-only and --review-exclude using their name attribute:
pytest --review --review-only=my-analyzer
pytest --review --review-exclude=my-analyzer
Acknowledgments
- The smells analyzer is inspired by the pytest-smell project from the dissertation "Detecting Test Smells in Python" by Maxim Pacsial.
- Test smell concepts are based on research by Van Deursen et al. ("Refactoring Test Code", 2001) and Meszaros ("xUnit Test Patterns", 2007).
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE for details.
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_review-0.1.4.tar.gz.
File metadata
- Download URL: pytest_review-0.1.4.tar.gz
- Upload date:
- Size: 142.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdf3b97430af4bd6417bd0d8a304970af2bd3ffd0e98fcf41af46f3924ba4692
|
|
| MD5 |
ee41ba65592a0c055b0f8b0dd28e35af
|
|
| BLAKE2b-256 |
d6d97121d4bfe072d9a89367f44266656f8e88f1afb9c2895d2271ad952c1469
|
File details
Details for the file pytest_review-0.1.4-py3-none-any.whl.
File metadata
- Download URL: pytest_review-0.1.4-py3-none-any.whl
- Upload date:
- Size: 50.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3760d45d54a9ce71af2b525117c0eb1eb8977808b060b4a36dc198f7eb2adbfc
|
|
| MD5 |
d0c6880463bb88958f1df6c1df7dae71
|
|
| BLAKE2b-256 |
71af895436eae9e0420e1e58b3a08f2a21cd7577be0639bdc7f6a86e9e46f607
|