skip to content

Why can a misspelled assertion on a unittest.mock Mock pass silently?

level: middleimportance: should knowfreq 44%

answer

  1. Attribute access on a mock never fails
  2. A typo becomes a new child mock
  3. The interpreter guards names that look like assertions
  4. Missing parentheses check nothing at all
  5. Negative assertions are vacuous on the wrong attribute

basics

~20 s

A Mock auto-creates any attribute you touch, so a misspelled assertion just returns a new child mock and checks nothing. CPython now rejects names that look like assertions, but forgetting the parentheses or typing the wrong attribute name still passes silently.

solid answer

~40 s

`Mock.__getattr__` invents a child mock for any attribute you access, so historically `results.assert_called_onse_with(3)` created an attribute, called it, discarded the result and reported success. CPython closed most of that: since 3.5 attribute names starting with `assert` or `assret` raise `AttributeError`, 3.12 added `asert`, `aseert` and `assrt`, and current versions also reject the bare assertion names such as `called_once_with` and `has_calls`. `Mock(unsafe=True)` opts back out. Two holes remain on 3.14: writing `mock.assert_called_once_with` **without parentheses** evaluates a bound method and checks nothing, and misspelling the *mocked attribute* makes a negative assertion vacuous — `differ.emmit.assert_not_called()` passes even though `differ.emit` was called. Defences are asserting positive facts, giving the mock a spec of the real object, and a linter rule for useless expression statements.

code

python · 12 lines
python
from unittest.mock import Mock

differ = Mock()
differ.emit("KJ118")

# 1. missing parentheses: a bound method is evaluated, then discarded
differ.emit.assert_called_once_with

# 2. a misspelled child mock makes a negative assertion vacuous
differ.emmit.assert_not_called()

print("both lines passed")

go deeper

for a junior

Remember the core mechanic: touching any attribute on a Mock creates it, so a typo used to check nothing. Know that modern Python raises AttributeError for names that look like assertions.

for a middle

Explain where the guard lives and what it covers: prefixes such as assert, assret, asert and the bare assertion names, disabled by unsafe=True. Then name what it still cannot catch, especially a missing pair of parentheses.

for a senior

Show how you would find such a test in a real suite: watching each assertion fail once, preferring positive assertions, constraining mocks to the real interface, and treating a never-red test as unverified.

for a principal

Own the policy angle: how a suite earns trust, whether silent-pass classes are caught by lint or by convention, and what evidence you require before a green build is allowed to gate a release.

A `Mock` is deliberately permissive: `Mock.__getattr__` creates a child mock for any attribute you touch and caches it, which is what lets you write `client.session.post(...)` against a mock without configuring anything. That same permissiveness is what makes a mistyped assertion dangerous, because an assertion is just an attribute access followed by a call — and both halves succeed on a mock no matter what you spell. ## The classic failure Imagine a flight-schedule differ whose publish step swallows an exception and quietly emits nothing. The test that was supposed to catch it reads `differ.sink.assert_called_onse_with(expected)`. On an old interpreter that access creates a child mock, calling it returns another mock, the statement value is discarded, and the test passes. The bug rides a three-week release train into production with a green suite behind it. The test was not weak; it never ran. ## What CPython did about it `unittest.mock` grew a guard in `__getattr__`. On Python 3.5 any attribute name starting with `assert` or `assret` raises `AttributeError` instead of being auto-created. Python 3.12 widened the prefix list to also cover `asert`, `aseert` and `assrt`. Current versions go further and reject a deny list built from the real assertion names with the `assert_` prefix stripped — so `called_once_with`, `has_calls`, `any_call` and `not_called` raise too, which catches the other common slip of dropping the prefix entirely. The error message names the attribute and suggests giving the mock a spec if you genuinely meant it as data. `Mock(unsafe=True)` disables the guard. That exists for the legitimate case where the object you are faking really does have an attribute called `assert_something`, and it should be rare — reaching for it to silence an error is how you re-open the hole. ## What still passes silently on 3.14 **Forgetting the parentheses.** `differ.sink.assert_called_once_with` — with no call — is a valid attribute access that yields a bound method and then throws it away. Nothing is checked, nothing raises, and the guard cannot help because the name is spelled correctly. This is the single most common surviving form. **A typo in the attribute being mocked, combined with a negative assertion.** `differ.emmit.assert_not_called()` auto-creates the child `emmit`, which was of course never called, so the assertion passes — while the real `differ.emit` was called all along. Every negative assertion on a mock is vacuous if the target name is wrong, and the guard only inspects the assertion name, not the attribute path in front of it. **Asserting on the wrong object.** `differ.assert_not_called()` passes whenever the code only ever called children of `differ`. ## How to make the failure loud * **Prefer positive assertions.** `assert_called_once_with(...)` on a mistyped attribute fails immediately with "Called 0 times", because it demands evidence rather than the absence of it. Pair every `assert_not_called()` with at least one positive assertion elsewhere in the test that proves you are holding the right mock. * **Constrain the mock to the real interface.** Giving a mock a spec of the object it stands in for makes any attribute the real object does not have raise `AttributeError`, which kills the mistyped-child case outright. (That mechanism has its own depth and its own tradeoffs; the point here is only that it closes this hole.) * **Make the test fail first.** A test that has never been seen red is not evidence. Break the production line the assertion covers and watch the assertion complain; a silent assertion is exposed instantly. * **Lint for pointless statements.** An expression statement whose value is unused — exactly what a parenthesis-less assertion is — is a standard lint finding, and a static type checker that knows the mocked type will also flag misspelled attributes. * **Watch mutation coverage or a canary.** If a suite has a habit of this, deliberately introducing a defect and confirming a red build is the cheapest audit you can run in review. ## Interview framing The question is really about two things at once: understanding that a mock's convenience comes from unconditional attribute creation, and knowing that "the test passed" and "the test checked something" are different claims. Candidates who answer only "there is a check for that now" have half of it; the strong answer names what the interpreter guard covers, what it cannot cover, and the review habits that catch the rest.

  • What does the unsafe=True argument to unittest.mock.Mock turn off, and when is it legitimate?
    It disables the `__getattr__` guard that raises `AttributeError` for attribute names that look like assertions. It is legitimate only when the object you are standing in for genuinely has such an attribute — a domain object with a method called `assert_valid`, say — so that the mock has to be able to auto-create it. Using it to silence the error on an ordinary test is how the silent-pass hole comes back.
  • How would you catch a parenthesis-less mock assertion in review or in CI?
    It is an expression statement whose value is discarded, which linters report as a useless statement, so a lint rule catches it mechanically. Beyond that, require that every new assertion has been observed failing at least once, and prefer positive assertions so a wrong or unexercised target fails on the spot rather than passing vacuously.
  • Why does differ.emmit.assert_not_called() pass even though differ.emit was called?
    `emmit` is a misspelling, so the mock auto-creates it as a fresh child with an empty call record, and asserting that an untouched mock was never called trivially succeeds. The guard only inspects the assertion name, not the attribute in front of it. Constraining the mock to the real interface, or asserting a positive fact about `differ.emit`, exposes the typo.

saying these in an interview costs you the question

  • Believes any misspelled assertion still raises on modern Python
  • Thinks a passing test proves the assertion actually executed
  • Uses unsafe=True to silence the AttributeError instead of fixing the name
  • Relies on assert_not_called without ever asserting a positive fact
  • Assumes attribute access on a Mock can fail for an unknown name by default
  • Writes the assertion without parentheses and calls it checked

context