Skip to content

Instantly share code, notes, and snippets.

@nvgoldin
Created April 20, 2020 06:57
Show Gist options
  • Save nvgoldin/21902d19c6283922e34d33f40e69e510 to your computer and use it in GitHub Desktop.
Save nvgoldin/21902d19c6283922e34d33f40e69e510 to your computer and use it in GitHub Desktop.
Why you should prefer to use `spec` in unittest.mock

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:

  1. 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.
  2. For "complicated" objects, it can make tests sometimes slow. Though that is usually not the case.
  3. There are limitations for what mock can spec, see the docs for more details
  4. 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.

TL;DR

Use spec whenever you can.

For more on that (with focus on autospec): https://docs.python.org/3/library/unittest.mock.html#auto-speccing

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