Specs and autospec

unittest · Expert · 12 min

Unspecced mocks keep passing after the real API changes, which is how mock-heavy suites rot.

A plain Mock accepts any attribute and any call signature. If the real send_email(to, subject) is renamed or gains a required argument, tests using a plain mock keep passing while production breaks.

Specs constrain a mock to the real object's shape:

  • Mock(spec=RealClass): accessing attributes that don't exist on the class raises AttributeError, and isinstance(m, RealClass) is True. Signatures are not checked.
  • spec_set=: also forbids setting non-existent attributes.
  • create_autospec(obj) or patch(..., autospec=True): recursively specs attributes and checks call signatures, for functions and methods. Calling with wrong arguments raises TypeError, just like the real thing. Methods are specced without self, and the result tracks the real signature.
  • seal(mock) stops a mock from creating new child attributes after you've configured it.

Limitations: autospec only sees attributes that exist on the class, so instance attributes set in __init__ are missing unless they are declared as class-level annotations or defaults. Autospeccing large objects costs some time, and properties or descriptors may be triggered during introspection.

Rule of thumb: default to autospec=True when patching your own code's functions and classes.

Example

from unittest.mock import Mock, create_autospec, patch, seal

class Mailer:
    def send(self, to: str, subject: str) -> bool:
        return True

Mock().sennd("x")                             # plain Mock: typo silently accepted

strict = create_autospec(Mailer, instance=True)
strict.send("a@x.io", subject="hi")           # matches the real signature
for bad in (lambda: strict.sennd("x"), lambda: strict.send("a@x.io")):
    try:
        bad()
    except (AttributeError, TypeError) as e:
        print(type(e).__name__, str(e)[:45])
# AttributeError Mock object has no attribute 'sennd'
# TypeError missing a required argument: 'subject'

m = Mock()
m.client.get.return_value = 1
seal(m)
try:
    m.client.post
except AttributeError as e:
    print("sealed:", e)                        # sealed: mock.client.post

Remember

  • spec= restricts attributes; spec_set also restricts assignment
  • autospec / create_autospec also enforces call signatures
  • seal() freezes a configured mock

Watch out for

  • Relying on autospec for instance attributes created in __init__; they don't exist on the class, so the spec rejects them.

Check yourself

  1. Why does patch(..., autospec=True) catch more bugs than a plain MagicMock?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/unittest.mock.html#autospeccing

Builds on

Unlocks

Nothing builds on this yet.