The Tautology Mock
This test passes even if you delete all your code. That's the signature of a tautology mock: the mock returns the exact value the test then asserts, so the mock is answering its own question. The suite stays green, coverage looks fine, and the production code never runs. AI coding assistants generate this pattern constantly — asked to "add a test," they stub every dependency and assert the stub — so you need a fast way to spot it.
Anatomy of a test that tests nothing
def test_get_total(mocker):
mocker.patch("cart.compute_total", return_value=42)
assert get_total(cart) == 42 # asserts the mock, not the code
Trace the data: the value 42 enters through the mock and exits through the assertion without touching a single line you wrote. compute_total could contain a bug, an exception, or nothing at all — the test can't tell. In Google's testing vocabulary these are close cousins of change-detector tests: tests that mirror the implementation, break on every refactor, and never catch a real defect.
The 10-second detector
One question exposes almost every tautology mock: could this test fail? Concretely:
- Delete-the-body check. Replace the function under test with
return 42(or comment out its body if a mock supplies its output). Still green? The test verifies nothing. - Red-first check. A legitimate new test fails before you write the code — that's the red step of red-green-refactor. If a fresh test passes on first run, it isn't testing anything.
- Trace the asserted value. If the expected value appears twice in the test file — once in a
return_value, once in anassert— and nowhere in between does production code transform it, you have a tautology.
What to use instead
| Situation | Reach for | Why |
|---|---|---|
| Pure logic, cheap dependencies | The real implementation | Highest fidelity; refactors don't break tests |
| Slow/external dependency (DB, API) | An in-memory fake with working behavior | Exercises real call paths without the network |
| You must verify an interaction happened | A mock — asserting the call, not an echoed value | Behavior verification is the one job mocks do well |
| Mocking anyway | autospec=True / spec | The mock rejects calls the real API would reject |
Martin Fowler's distinction is the key: a fake has a working implementation (an in-memory repository that really stores and retrieves), while a stub provides canned answers. Fakes can be wrong in interesting ways — which is exactly what makes tests against them meaningful. Google's testing team has pushed the same line for a decade: prefer real implementations, then fakes, and mock only what you can't run.
Power-user moves
- Autospec everything you mock.
unittest.mock.patch(..., autospec=True)copies the real signature, somock_fn("wrong", "args")raisesTypeErrorinstead of silently succeeding. A bareMock()accepts any call with any arguments — the perfect environment for tests that can't fail. - Keep one fake per boundary, in production-adjacent code. A shared
InMemoryOrderRepoused by 40 tests is maintained and trusted; 40 ad-hocmocker.patchcalls are 40 tautologies waiting to happen. - Mutation-test the suspects. Run a mutation tool (or just hand-mutate: flip an operator, off-by-one a boundary) on modules with heavy mocking. Surviving mutants are tests that assert mocks, not logic.
- Gate AI-generated tests on the red step. When an assistant writes tests for existing code, ask it to also show each test failing against a stubbed-out implementation. If it can't produce a failure, reject the test.
Resources
- Mocks Aren't Stubs — Martin Fowler
- Testing on the Toilet: Don't Overuse Mocks — Google Testing Blog
- Change-Detector Tests Considered Harmful — Google Testing Blog
- Increase Test Fidelity By Avoiding Mocks — Google Testing Blog
- unittest.mock —
return_value,spec, andautospec(Python docs)
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.