Skip to content

Instantly share code, notes, and snippets.

@slinkp
Last active August 10, 2023 10:11
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slinkp/c3ec1f47d7ecfe682ad4 to your computer and use it in GitHub Desktop.
Save slinkp/c3ec1f47d7ecfe682ad4 to your computer and use it in GitHub Desktop.
How to use Mock Specs Properly with Classes
"""
TL/DR:
Never instantiate mock.Mock() directly.
Instead use either mock.create_autospec(YourClass) OR mock.patch('YourClass', autospec=True).
The "spec" feature of Mock is great, it helps avoid your mocked API drifting out of sync with your real API.
But there is a gotcha if you are mocking classes directly - it's easy to mock the class but you need to
ensure the spec applies to the *instance* as well.
"""
>>> import mock
>>> class Foo(object):
... def bar(self): pass
...
>>> WrongMockFoo = mock.Mock(spec=Foo)
>>> WrongMockFoo.bar()
<Mock name='mock.bar()' id='17171024'>
>>> WrongMockFoo.other() # This should fail... and it does!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 658, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'other'
"""
That's great, but what about instances of the mocked class?
"""
>>> foo = WrongMockFoo()
>>> foo.bar()
<Mock name='mock().bar()' id='17171408'>
>>> foo.other() # uh-oh, this should fail, but the instance doesn't inherit the class spec
<Mock name='mock().other()' id='17171664'>
"""
To fix this so instances work properly, use create_autospec().
"""
>>> BetterMockFoo = mock.create_autospec(Foo)
>>> foo = BetterMockFoo()
>>> foo.bar()
<MagicMock name='mock().bar()' id='17171792'>
>>> foo.other()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 658, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'other'
"""
And if you're using mock.patch(), just pass autospec=True.
"""
>>> with mock.patch('__main__.Foo', autospec=True) as AlsoBetterMockFoo:
... foo = AlsoBetterMockFoo()
... print foo.bar()
... print foo.other() # this should fail!
...
<MagicMock name='Foo().bar()' id='17136656'>
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 658, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'other'
"""
Speccing the mock also helps catch call signature errors.
"""
>>> foo.bar('uhoh')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/dist-packages/mock.py", line 954, in __call__
_mock_self._mock_check_sig(*args, **kwargs)
TypeError: <lambda>() takes exactly 1 argument (2 given)
>>>
@jbasko
Copy link

jbasko commented Jan 10, 2019

Thanks for the nice step through.

I have come across a problem that create_autospec creates a mock which doesn't catch incorrect initialisation calls. Initialisation is checked only if you have an explicit __init__ method defined in the class.

In [2]: class Foo:
...:     pass
...:

In [3]:

In [3]: MockFoo = mock.create_autospec(Foo)

In [4]: MockFoo(23)
Out[4]: <__main__.Foo at 0x105b60cf8>

In [5]: MockFoo(23, 23)
Out[5]: <__main__.Foo at 0x105b60cf8>

In [6]: MockFoo(x=23)
Out[6]: <__main__.Foo at 0x105b60cf8>

With explicit __init__:

In [7]: class Bar:
...:     def __init__(self):
...:         pass
...:

In [8]: MockBar = mock.create_autospec(Bar)

In [9]: MockBar(23)

[..explosion details skipped..]
TypeError: too many positional arguments

@a8568730
Copy link

Thanks for introducing the create_autospec, the examples are clear and helpful. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment