Python mocks should always have a spec

31 Jul 2022

If you’ve worked with Python tests for a while you’ve probably had a moment where you encountered an assertion that looks like one of these:

assert mock.called_once()
...
mock.assret_called_once()

Because Python is often so very readable, and because these two lines look plausible at a glance, these assertions will lie in wait right until you need to make changes to the code that they test. At that point one of two things are likely to happen: if you’re doing test-driven development you might be confused as to why these assertions are still passing; or if you’re not doing test-driven development you’ll see a green test suite report, go on to deploy your changes and then see a bunch of errors.

The problem, of course, is mocks. When you use unittest.mock.patch you’ll end up with a mock object that is dangerously permissive. It doesn’t care that called_once may not be an existing method on itself or the thing it’s mocking, or that assret_called_once is a typo, it will always return a new mock object each time and that object will always evaluate as True in an assert statement. This eventually became such a problem that common typos were actually hard-coded into the standard library in Python 3.5 and now raise an AttributeError! This is a helpful improvement, but it only helps that narrow case.

Suppose you’re testing a class you’ve written:

# test.py
from unittest import TestCase, mock

class MyClass:
    pass


def some_code_under_test():
    MyClass().non_existent_method()


class TestMyClass(TestCase):

    @mock.patch('test.MyClass')
    def test_my_class(self, klass):
        some_code_under_test()  # would blow up outside this test

        # This should also fail, `called_once` is incorrect
        assert klass.return_value.non_existent_method.called_once()

This test will pass just fine even though MyClass has no methods defined on it at all, which likely isn’t the behaviour you want. To take a slightly more realistic example:

from typing import Dict, Tuple

import requests


class ThirdPartyClient:

    def patch(self, identifier: str, data: Dict[str, str]) -> requests.Response:
        url = f'https://www.third-party.com/api/v1/product/{identifier}/'

        response = requests.patch(url, json=data)
        response.raise_for_status()

        return response


def update_third_party_api(identifier: str, data: Dict[str, str]) -> bool:
    client = ThirdPartyClient()

    try:
        client.patch(identifier, data)
    except requests.RequestException:
        return False
    else:
        return True

You want to test that update_third_party_api returns the correct value in each scenario, but you don’t want your test to actually call out to the third party, so you reach for a mock.

from unittest import TestCase, mock


class TestUpdateThirdPartyApi(TestCase):

    @mock.patch.object(ThirdPartyClient, 'patch')
    def test_returns_true_if_update_succeeds(self, third_party_patch):
        response = requests.Response()
        response.status_code = 200
        response._content = b'Success!'
        third_party_patch.return_value = response

        assert update_third_party_api('an-identifier', {'new': 'data'}) is True
        third_party_patch.assert_called_once()

This test passes, and is a reasonable enough test to have written even if you were doing test-driven development. Let’s say that further down the line, though, that the third party updates their client and the function signature for ThirdPartyClient.patch changes – it now expects you to pass in a timeout:

class ThirdPartyClient:

    def patch(
        self,
        identifier: str,
        data: Dict[str, str],
        timeout: Tuple[float, float],  # new required argument
    ) -> requests.Response:
        url = f'https://www.third-party.com/api/v1/product/{identifier}/'

        response = requests.patch(url, json=data, timeout=timeout)
        response.raise_for_status()

        return response

Your test will still pass even though the code in update_third_party_api is now calling ThirdPartyClient.patch incorrectly! You’ll only end up finding out once you’ve deployed the code and you start getting errors, if not complaints from users.

Jump to heading autospec to the rescue

If you pass autospec=True to your mock.patch.object decorator then you’ll end up with a much less permissive mock, one that expects to be called correctly:

TypeError: missing a required argument: 'timeout'

Now you can truly be confident that your tests have got you covered when your code is broken. A specced mock will also catch non-existent mock methods like assert third_party_patch.called_once(), which I’ve come across a surprising number of times in some of our test suites:

AttributeError: 'function' object has no attribute 'called_once'

If you’re creating mocks outside of a decorator then you can use create_autospec to achieve the same ends.

Jump to heading Limitations of autospec

You might reasonably ask why autospec=True isn’t the default for all new mocks, or at least why you wouldn’t use it on all of yours. As with most things in this job there are always gotchas and caveats to consider.

Jump to heading Unintended side effects

If you’re mocking a class then create_autospec and autospec=True work by calling getattr(original, attribute) for every attribute on original. If you’ve overridden the __getattribute__ method on your class for whatever reason then creating a mock may end up having some surprising consequences:

def __getattribute__(self, name: str) -> Any:
    print('side effect!')
    return super().__getattribute__(name)
$ pytest test.py
...

----------------------- Captured stdout call -----------------------
side effect!

Jump to heading Performance implications

Because creating a spec involves looping over every attribute on an object this can have an impact on the performance of your test suite if the object in question is very large and/or it is mocked in a large number of tests. On my machine an object with 50 attributes that is mocked in 5,000 tests will add a little over two seconds to your test suite:

An autospecced class with 10 attributes will add 1.66s over 5000 tests via create_autospec
An autospecced class with 10 attributes will add 1.67s over 5000 tests via the patch decorator
An autospecced class with 25 attributes will add 1.78s over 5000 tests via create_autospec
An autospecced class with 25 attributes will add 1.86s over 5000 tests via the patch decorator
An autospecced class with 50 attributes will add 2.11s over 5000 tests via create_autospec
An autospecced class with 50 attributes will add 2.24s over 5000 tests via the patch decorator

The patch decorator seems a little slower than running create_autospec, but not significantly so. If you need/want your test suite to be blindingly fast then you might want to consider where it’s most valuable to use autospeccing.

Jump to heading Dynamic attributes

Looping over all the attributes returned by an object’s dir method works perfectly for methods and attributes that are declared at runtime, but will end up missing anything that’s created dynamically, in the __init__ method for example:

class ThirdPartyClient:

    def __init__(self):
        self.version = '1.0.0'

    ...


    @mock.patch.object(ThirdPartyClient, 'version', autospec=True)
    def test_version_specific_logic(self, third_party_version):
        ...

Try to mock version you’ll end up with the following error:

AttributeError: <class 'test.ThirdPartyClient'> does not have the attribute 'version'

I ran into this recently with the Session class from the requests package because the auth and headers attributes are declared dynamically, so your specced mock will throw an error when you try to access them in your test. In some cases this might indicate that either your code or your test isn’t implemented quite right, if not both, or you might decide that the security that speccing provides isn’t necessary in this use case.

Jump to heading Return values

This one feels slightly uncharitable to autospec, but it’s worth remembering that the return value of mocked methods will always end up being a permissive mock by default unless you specify otherwise, even if you’ve applied autospec=True in what might look like the right place:

class MyClass:
    pass


class OtherClass:
    def get_my_class_instance(self):
        return MyClass()


def some_code_under_test():
    instance = OtherClass().get_my_class_instance()
    instance.non_existent_method()


class TestMyClass(TestCase):

    @mock.patch('test.OtherClass', autospec=True)
    def test_my_class(self, klass):
        some_code_under_test()  # would blow up outside this test

In the not-very-realistic example above MyClass has no method called non_existent_method, so when some_code_under_test is running outside of your test then it will blow up. You might think that mocking OtherClass with autospec=True would help you here but it won’t, the return value of get_my_class_instance will end up being a permissive mock that won’t check the attributes on MyClass. If you want to fix that then you need to get terribly specific about what get_my_class_instance should return in your test:

class TestMyClass(TestCase):

    @mock.patch('test.OtherClass', autospec=True)
    def test_my_class(self, klass):
        my_klass = mock.create_autospec(MyClass)
        klass.return_value.get_my_class_instance.return_value = my_klass

        some_code_under_test()  # now this will fail

It’s not really reasonable to expect the built-in speccing to catch this for you, but I wanted to include it just in case you thought that autospec makes you invincible!

Jump to heading Conclusion

Without getting into the extremely murky waters of when you should and shouldn’t use mocks in Python, about which many words have been and will be written, the chances are that you’re going to encounter a number of them in whatever codebase you’re working on. Using autospec and create_autospec should, in my opinion, be your default way of constructing these mocks because it provides a much-needed safety net in your tests that persists over time as interfaces naturally develop. Depending on what you’re mocking it can have some significant downsides, but for most use cases it’s as close to a testing no-brainer as you’re going to find.