Understanding mock.assret

Do you know those coding tasks that are quite common but never remember how to do it step by step and we always have to Google it? Writing unit tests using the Python module unittest.mock is one of those tasks. I never remember if I should import patch from unittest or if it’s part of unittest.mock or, which are all available assert methods.

This time I wanted to test if, during a function call, another function was getting called once and with the right arguments. A simple but perfect example where Mock would be the right answer, but only if I knew which method to use. I didn’t.

I knew that the assert functions of mock start with the assert_ prefix. So I went to Python’s documentation, which is now available to Brazilian Portuguese, and while I was searching by assert_, I saw the following example:

>>> mock = Mock(name='Thing', return_value=None)
>>> mock(1, 2, 3)
>>> mock.assret_called_once_with(4, 5, 6)

Although I usually forget the method names, I feel comfortable using the unittest module and I handle myself just fine creating tests with it. So the moment I saw the example, I knew assert_called_once_with was exactly the method I needed for my test. But I also saw a typo in the documentation, where assert was written as assret.

As someone that loves to contribute to open source projects, I immediately thought that I could just fix that typo. So I created a fork of Python source code, went directly to the file unittest.mock.rst in the Doc folder, and searched by assret. I was shocked: 7 words were found. That was strange, how they wrote the same typo 7 times in the documentation? How did nobody see that during pull request reviews?

My curiosity led me to the wrong path. Instead of stopping and properly reading the docs, I immediately started reading the commit history of those typos. And then everything became clear:

bpo-41877 Check for asert, aseert, assrt in mocks (GH-23165) - 4662fa9b

Currently, a Mock object which is not unsafe will raise an AttributeError if an attribute with the prefix assert or assret is accessed on it. This protects against misspellings of real assert method calls, which lead to tests passing silently even if the tested code does not satisfy the intended assertion.

Recently a check was done in a large code base (Google) and three more frequent ways of misspelling assert were found causing harm: asert, aseert, assrt. These are now added to the existing check.

In reality, when mentioning assret, the documentation is describing a feature in Python that makes the usage of mocks safer by its users!

Because of its nature, we can say that a mock can assume any form, or more specifically, any method or attribute required in a context. However, mocks also have their own attributes and methods that can be used, for example, to check if a particular function has been called and with which arguments.

In [1]: requests = Mock()

In [2]: requests.get("https://rgth.co")
Out[2]: <Mock name='mock.get()' id='4374546224'>

In [3]: requests.get.called
Out[3]: True

In [4]: requests.get.assert_called()

In the example above, in the first line, I created a new instance of Mock to simulate the library requests. In the second line, I called the method get(), faking an HTTP request of type GET to the URL https://rgth.co. As the variable requests, in this case, is just a mock instance, no actual request has been done, but in lines three and four, I confirmed that the method get() has been called.

While creating unit tests, something quite similar to this example can be done when we need to verify the behaviour of a function that depends on HTTP requests. For those cases, the request per si doesn’t matter, but how the function depends on it behaves.

A função assert_called() não retorna nada se o método testado foi executado, porém gera uma exceção no caso oposto. Seguindo o exemplo, como o método post não foi executado, chamar assert_called() gera a exceção AssertionError:

The methods assert_called() returns None if the method being tested has been called, but raises an exception when it hasn’t. Following the example, as the method post() has not been called, using assert_called() raises the AssertionError exception:

In [6]: requests.post.assert_called()
AssertionError                            Traceback (most recent call last)
/.../lib/python3.9/unittest/mock.py in assert_called(self)
    874             msg = ("Expected '%s' to have been called." %
    875                    (self._mock_name or 'mock'))
--> 876             raise AssertionError(msg)
    878     def assert_called_once(self):

AssertionError: Expected 'post' to have been called.

If we write requests.post.assret_called() in the code above, with the assret typo, no exception would raise as we expect. This would be an unsafe operation because a test would silently pass even if the code has different behaviour.

But that is not actually the case when using Python, or unittest built-in library. The most common typos when writing assert are verified when using mocks. If I do write a typo like assret_called(), Python would raise an exception telling me where to fix it:

In [7]: requests.post.assret_called()
Traceback (most recent call last):
AttributeError: 'assret_called' is not a valid assertion. Use a spec for the mock if 'assret_called' is meant to be an attribute.. Did you mean: 'assert_called'?

The source code of this check is quite simple and can be read here. The important part is:

if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')):
    raise AttributeError(
        f"{name!r} is not a valid assertion. Use a spec "
        f"for the mock if {name!r} is meant to be an attribute.")

I was thrilled to see how such a simple feature of huge impact, manifest so well one of the Python Zen principles: Errors should never pass silently.