Mockrot (pytest-mockrot)
Mockrot is a pytest plugin that detects rotting mocks : unit test mocks whose call signatures, return types, or dictionary payloads have silently drifted away from real production contracts.
Zero configuration required. Pure transparency. Zero false positives.
The Problem: The Green Test Illusion
In Python testing, unittest.mock.patch and pytest-mock are ubiquitous. However, standard mocking tools create a dangerous false sense of security:
# 1. Production code evolves: return type changed from dict to Pydantic model
def fetch_invoice(invoice_id: str) -> InvoiceReceipt: ...
# 2. Legacy unit test written months ago:
@patch("app.billing.fetch_invoice", return_value={"id": "inv_123", "amount": "invalid_str"})
def test_checkout(mock_fetch):
result = checkout_cart("inv_123")
assert result is True
- The test is 100% green. Standard
unittest.mocknever verifies thatreturn_valueconforms toInvoiceReceipt. - Even
autospec=Trueonly checks call parameter names—it never validates return values. - Production crashes with
ValidationError,AttributeError, orKeyError.
This issue is amplified when AI coding assistants refactor production signatures: existing unit tests keep passing on obsolete mock data while production breaks.
The Solution: Bidirectional Contract Verification
Mockrot intercepts mocks during test execution, extracts the true production contract (inspect.signature + typing.get_type_hints), and validates both inputs and outputs at runtime using Pydantic v2 TypeAdapter:
pytest execution
│
▼
interceptor.py ──▶ Captures real target & mock calls
│
▼
extractor.py ──▶ Resolves signature, type hints & async contracts
│
▼
validator.py ──▶ Validates parameters, return types & zombie fields
│
▼
reporter.py ──▶ Displays actionable terminal diffs without emojis
Key Features
- Call Signature Validation: Catches unexpected keyword arguments, missing required parameters, and positional mismatches.
- Strict Return Type Validation: Uses Pydantic v2
TypeAdapterin strict mode to ensure scalar types (int,str,bool), Pydantic models, dataclasses, andTypedDictare respected. - Zombie Field Detection: Identifies obsolete keys in mocked dictionaries that no longer exist in the target model (
BaseModel,dataclass, orTypedDict). - Async & Coroutine Mismatch Detection: Flags asynchronous targets mocked with synchronous values or un-awaited coroutines.
- Side Effect Verification: Validates every item produced by
side_effectiterables and callables while preserving legitimate exception simulation. - Fail-Safe Architecture: Internal errors in Mockrot are caught at an isolated boundary and converted to warnings—Mockrot will never crash your test suite.
- Negligible Overhead: Compiled schemas and TypeAdapters are cached per target (< 5.5 µs per validation, ~25 ms overhead over 500 tests).
- Line-Level Exemptions: Easily bypass legacy or intentional mock mismatches with
# mockrot: ignore.
Installation
# With pip
pip install pytest-mockrot
# With uv
uv add --dev pytest-mockrot
# With poetry
poetry add --group dev pytest-mockrot
Or run directly without installing using uvx:
uvx --from pytest-mockrot mockrot
Quickstart
1. Audit Mode (Default)
Run your tests with --mockrot. Mockrot runs your entire test suite without failing tests, and summarizes any rotting mocks at the end of the session:
pytest --mockrot
# or explicitly
pytest --mockrot=warn
2. Strict CI Mode (Gatekeeper)
In your CI/CD pipeline, enforce that no rotting mocks enter your codebase. Any contract violation immediately fails the test session with exit code 1:
pytest --mockrot=strict
Example Terminal Output
When a mock drifts from its real contract, Mockrot provides a clean, actionable report:
Mockrot detected rotting mock (return_type_drift):
Target: app.billing.fetch_invoice
Site: tests/unit/test_checkout.py:42 (test_checkout)
Error: 1 validation error for InvoiceReceipt
amount_cents
Field required [type=missing, input_type=dict]
Expected: InvoiceReceipt
Actual: dict: {'id': 'inv_123', 'amount': 'invalid_str'}
Type Drift Comparison
╭───────────────────┬─────────────────────────────╮
│ Expected Contract │ Actual Mock Output │
├───────────────────┼─────────────────────────────┤
│ InvoiceReceipt │ dict: {'id': 'inv_123', ...} │
╰───────────────────┴─────────────────────────────╯
Zombie Field in Mocked Dictionary:
Target: app.users.get_profile
Site: tests/unit/test_users.py:88 (test_profile)
Error: Zombie field 'legacy_token' detected in mocked dictionary for UserProfile.
This field is not declared on the production contract.
Ignoring Known Mocks
If a test intentionally mocks an invalid signature (e.g. testing error handling or legacy compatibility), you can exempt it:
Inline Comment (Recommended)
with patch("app.orders.calculate_taxes", return_value="invalid") as mock_calc:
mock_calc(100, 0.2) # mockrot: ignore
Pytest Marker
@pytest.mark.mockrot_ignore
def test_legacy_edge_case(): ...
Configuration
Mockrot requires zero configuration out of the box. You can optionally customize its behavior in pyproject.toml:
[tool.mockrot]
mode = "strict" # Options: "warn" (default) or "strict"
exclude_targets = [
"external_legacy_lib.*",
]
Comparison
| Feature | unittest.mock |
unittest.mock(autospec=True) |
pytest-mockrot |
|---|---|---|---|
| Signature verification | Partial | Yes | Yes |
| Return type validation | No | No | Yes (Pydantic v2 strict) |
| Zombie field detection | No | No | Yes |
| Async coroutine validation | No | No | Yes |
| Zero configuration | Yes | No (autospec=True needed) |
Yes (pytest --mockrot) |
| Terminal diff diagnostic | No | No | Yes (Rich formatting) |
Contributing
We welcome contributions! Please see CONTRIBUTING.md for local setup, testing, and contribution guidelines.
# Clone the repository
git clone https://github.com/ademboukabes/Mockrot.git
cd Mockrot
# Install dependencies with uv
uv sync
# Run format, lint, type checks, and tests
uv run ruff format --check .
uv run ruff check .
uv run pyright
uv run pytest --cov=mockrot --cov-branch
License
This project is licensed under the terms of the MIT License.
Metadata
Release files for pytest-mockrot 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_mockrot-0.1.2.tar.gz | 113.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_mockrot-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 136.1 kB
Release files / pytest_mockrot-0.1.2.tar.gz
| Download URL | pytest_mockrot-0.1.2.tar.gz |
|---|---|
| Size | 113.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a00e5795514f5f57f8fabe6c44a0602193f9d15f55afd3cbaedb249675165e29
|
|
BLAKE2b-256 checksum How to use checksums |
e082a3936f224737154d2873ace51d028248e67206f5de3c6938c084f6e550c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pytest_mockrot-0.1.2-py3-none-any.whl
| Download URL | pytest_mockrot-0.1.2-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8bf103adc9a2dae859468034f26eb1eeabe7e939e8dd32268d0a9bc40f66720a
|
|
BLAKE2b-256 checksum How to use checksums |
cd8ad9b75f01eee86d35d752c27fb9750a2d779c7cfc50e9955b6d1f60935834
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|