64.3 spec= and create_autospec(): Safer Mocks
Right, so you’ve decided to use mocks. Good for you. It means you’re testing behavior, not just state, and that’s a sign of a mature test suite. But let’s be honest: the standard unittest.mock library gives you enough rope to hang yourself with, and then some. You can mock anything, anywhere, anytime. That’s not power; that’s a liability. Ever written a test that passes beautifully, only to have the production code explode because your mock was completely divorced from the reality of the function it was pretending to be? I have. It’s a special kind of humiliation.
The core problem is that when you use Mock() directly, you’re creating a blank slate. It will happily accept any call, with any arguments, and return whatever you told it to. It doesn’t know what the real object is supposed to look like. If you change the signature of the real function—say, add a new required argument—your mock, blissfully ignorant, will carry on without a care in the world. Your test still passes. Your code breaks in production. This is the opposite of helpful.
This is where spec and its smarter, more aggressive cousin create_autospec() come in. They’re your mock’s conscience, preventing it from telling lies that are a little too convenient.
Using spec to Keep Your Mocks Honest
The spec parameter is your first line of defense. You pass it an object (a class, a function, another mock), and the new mock will only allow you to access attributes and methods that exist on that “spec” object. It’s like giving your mock a job description so it knows what it’s actually allowed to do.
from unittest.mock import Mock
class DatabaseConnection:
def commit(self):
pass
def rollback(self):
pass
# A dangerous, unconstrained mock
bad_mock = Mock()
bad_mock.commit() # Works
bad_mock.comit() # Also "works" - typo! This is a silent failure.
# A safer mock with a spec
good_mock = Mock(spec=DatabaseConnection)
good_mock.commit() # Works, it's on the spec
good_mock.rollback() # Also works
# This will now raise an AttributeError immediately. Thank goodness.
try:
good_mock.comit()
except AttributeError as e:
print(f"See? The spec caught our typo: {e}")
The key here is that it fails fast. You get the error right in your test, at the point where you try to set up the bad mock, not later in some obscure way. This is infinitely better than a test that passes with a mock that doesn’t resemble reality.
create_autospec(): The Spec on Steroids
While spec is great for constraining attributes, create_autospec() goes the whole hog. It doesn’t just create a mock with a spec; it creates a mock that also has the same call signature as the object you’re mimicking. This is the killer feature for preventing signature drift.
from unittest.mock import create_autospec
def my_function(important_arg, optional_kwarg=None):
return important_arg * 2
# The old, dangerous way
mock_old = Mock(return_value=100)
mock_old(1, 2, 3, "this is all nonsense") # No problem. Sad.
# The new, safety-conscious way
safe_mock = create_autospec(my_function, return_value=100)
safe_mock(1) # Perfectly fine, matches the signature.
# This will raise a TypeError because the signature is wrong.
try:
safe_mock() # Missing required argument
except TypeError as e:
print(f"Autospec protects us: {e}")
This is a game-changer. If a developer goes into the real my_function and changes its signature—adding, removing, or renaming arguments—any test using create_autospec will immediately fail with a clear TypeError. It forces the test to stay in sync with the code it’s mocking. This isn’t just a testing tool; it’s a design and refactoring aid.
When Autospec Gets It Wrong (And What to Do About It)
Now, create_autospec isn’t perfect. It can be overly strict in some scenarios. The most common headache is with @property decorators. A real property is accessed as an attribute, but a mock created with autospec will still treat it as a callable, which will cause an error.
class MyClass:
@property
def my_prop(self):
return "value"
spec_mock = create_autospec(MyClass)
spec_mock.my_prop # This is a Mock object, ready to be called.
# spec_mock.my_prop() # This would work, but it's wrong.
# The real object doesn't work that way!
real_obj = MyClass()
real_obj.my_prop # Returns "value"
# real_obj.my_prop() # TypeError: 'str' object is not callable
To fix this, you have to manually configure the mock for that property to behave correctly.
from unittest.mock import PropertyMock
spec_mock = create_autospec(MyClass)
# Configure the property mock to return a value when accessed, not called.
type(spec_mock).my_prop = PropertyMock(return_value="fake_value")
print(spec_mock.my_prop) # Now returns 'fake_value' correctly.
It’s a bit clunky, I’ll admit. The designers gave us a brilliant safety feature but forgot that properties are a thing humans use. You just have to be aware of this edge case.
The bottom line is this: unless you have a very good reason not to, always use spec or create_autospec. The minimal extra setup is a tiny price to pay for tests that are actually coupled to your real code’s interface. It transforms your mocks from loose cannons into precise, reliable stand-ins. It’s the difference between a test suite that lulls you into a false sense of security and one that genuinely has your back.