So back to spec
and why its important:
Basically when you create a MagicMock, you have an object which is not bounded to any interface. This makes testing easy, but on the other hand, your code does use interfaces and they do change over time.
Here is a quick example:
Lets say I'm using a class called HashAPI
which provides hashing services and exposes the following interface:
class HashAPI:
def create_hash(self, key):
return hash(key)
My own class, MyFileStore
, allows you to pass an instance compatible with HashAPI
:
class MyFileStore:
def __init__(self, hasher):
self._hasher = hasher
def get(self, path_):
return self._hasher.create_hash(path_)
And I wrote the following tests:
class TestMyFileStore:
def test_get(self):
mock_hasher = mock.MagicMock()
mock_hasher.create_hash.return_value = 'some_hash'
store = MyFileStore(
hasher=mock_hasher,
)
assert store.get('some_path') == 'some_hash'
mock_hasher.create_hash.assert_called_with('some_path')
Looks good, right?
Now, for the sake of the example, lets say HashAPI
is an external library (obviously it doesn't need to be), and after a while, someone removed create_hash
and provide a new method instead:
class HashAPI:
def create_safe_hash(self, key):
pass
Now, even though the method create_hash
doesn't exist anymore, my tests in TestMyFileStore
- would still pass!!!
The solution to this, is specing
- create a mock
object which has the same interface as the object its trying to replace. unittest.mock
has built-in support for that, replace:
mock_hash = mock.MagicMock()
with:
mock_hash = mock.MagicMock(spec=HashAPI)
Going back to our example - this would have caused the test to fail when the method was removed.
Moreover, there is also the autospec
option, when mocking instances.
There are some drawbacks though:
- In order to
spec
mock needs to introspect the class, and some libraries might have side-effects, for example - opening connections. Obviously it is a bad design pattern to have side-effects on introspecting, but it happens. - For "complicated" objects, it can make tests sometimes slow. Though that is usually not the case.
- There are limitations for what
mock
canspec
, see the docs for more details - Sometimes, you are not sure what is the object you need to
spec
. Although this can be a problem - it is worth checking whether something else is wrong.
Use spec
whenever you can.
For more on that (with focus on autospec
): https://docs.python.org/3/library/unittest.mock.html#auto-speccing