Pytest plugin to prevent mocking of critical functions in tests.
Inspired by Increase Test Fidelity By Avoiding Mocks (Google Testing Blog) and the "Test Doubles" chapter of Software Engineering at Google.
Test fidelity is how closely a test's behavior resembles production. Mocks are cheap and fast, but every mock is a guess about how a dependency behaves. The Google guidance is a simple preference order:
- Real implementation — highest fidelity, run the actual code.
- Fake — a lightweight, working implementation (e.g. in-memory DB) maintained alongside the real one.
- Mock — last resort, when the real thing and a fake are both out of reach.
The problem in practice: once unittest.mock is in the toolbox, step 3 becomes step 1. Tests pass, coverage looks great, and bugs ship anyway because nothing real ran. This plugin makes it so that you can enforce step 1.
- Core domain logic where a mocked test gives false confidence.
- Functions that already have a fake or in-memory implementation available.
- Integration-style tests where swapping the real call for a mock defeats the purpose of the test.
- Codebases where mocks have historically drifted from reality and caused production incidents.
- Unit tests for code whose only job is to orchestrate external I/O, those are exactly the cases the Google article says mocks are legitimate for.
- Error paths that are genuinely hard to trigger otherwise.
- Fast feedback loops where a real dependency would turn a small test into a medium or large one.
- As a blanket ban. The marker is opt-in per test on purpose, it should be used when and where needed.
pip install pytest-do-not-mockThe plugin activates automatically once installed — no configuration needed.
Use @pytest.mark.do_not_mock with no arguments to prevent any mocking:
import pytest
@pytest.mark.do_not_mock
def test_payment_integration():
"""This test must use real implementations, no mocks allowed."""
result = process_payment(100.0)
assert result is TrueAny attempt to use Mock(), MagicMock(), patch(), or similar will raise DoNotMockError.
Use protect= to pass function objects, or positional args for string paths:
from myapp import process_payment, send_email
# Single function object
@pytest.mark.do_not_mock(protect=process_payment)
def test_selective():
"""process_payment cannot be mocked, but other functions can."""
with patch("myapp.send_email"): # this is fine
result = process_payment(100.0)
assert result is True
# Multiple function objects
@pytest.mark.do_not_mock(protect=[process_payment, validate_user])
def test_multiple():
...
# String module paths (positional args)
@pytest.mark.do_not_mock("myapp.payments.charge")
def test_string_path():
...
# Mixed
@pytest.mark.do_not_mock("myapp.send_email", protect=process_payment)
def test_mixed():
...# All tests in this class
@pytest.mark.do_not_mock
class TestPaymentIntegration:
def test_charge(self): ...
def test_refund(self): ...
# All tests in this module
pytestmark = pytest.mark.do_not_mockMarkers stack across scopes. When do_not_mock is applied at more than one level (module, class, function), every marker is honored — the function-level marker does not replace the outer ones.
# Protected at module scope
pytestmark = pytest.mark.do_not_mock("myapp.db.save")
@pytest.mark.do_not_mock("myapp.api.send")
class TestThing:
# This test protects all three: db.save, api.send, and email.notify
@pytest.mark.do_not_mock("myapp.email.notify")
def test_x(self): ...A bare marker at any scope (no args, no protect=) wins over targeted inner markers and blocks all mocking for tests it covers.
No-args mode (@pytest.mark.do_not_mock):
Mock(),MagicMock(),AsyncMock()patch()as decorator, context manager, orstart()/stop()patch.object(),patch.dict()create_autospec()
Targeted mode (@pytest.mark.do_not_mock(protect=func)):
patch()targeting the protected functionpatch.object()targeting the protected function- Other mocking is allowed
git clone git@github.com:LifeLex/pytest-do-not-mock.git
cd pytest-do-not-mock
python3 -m venv .venv
source .venv/bin/activate
make installmake help Show all commands
make install Install in editable mode with dev dependencies
make test Run tests
make lint Run ruff linter
make format Run ruff formatter
make typecheck Run mypy and pyright
make check Run all checks (lint, format, types, tests)
make clean Remove build artifacts
make build Build source and wheel distributions
make check # everything: lint + format + typecheck + tests
make lint # ruff linter only
make typecheck # mypy + pyright
make test # pytest onlyOr via tox for multi-version testing:
tox # all environments (py310-py313, linting, typing)
tox -e py313 # single Python versionsrc/pytest_do_not_mock/
├── __init__.py # Public API: DoNotMockError
├── errors.py # DoNotMockError exception
├── plugin.py # Pytest marker + hookwrapper (entry point)
├── guards.py # Mock interception and guard context manager
└── protected.py # ProtectedFunc resolution and validation
tests/
├── conftest.py # Shared fixtures and example app code
├── test_plugin.py # Marker registration, error messages, cleanup
├── test_block_all.py # Block-all mode (every mock/patch variant)
├── test_targeted.py # Targeted mode (protect=, string paths)
└── test_scopes.py # Class-level and module-level markers
- Tag the commit:
git tag v0.1.0 && git push origin v0.1.0 - GitHub Actions builds and publishes to PyPI automatically via trusted publishing
MIT