Monday, March 30, 2020

BreadcrumbsCollector: How to mock in Python? – (almost) definitive guide

What is a mock?

Mock is a category of so-called test doubles – objects that mimic the behaviour of other objects. They are meant to be used in tests to replace real implementation that for some reason cannot be used (.e.g because they cause side effects, like transferring funds or launching nukes). Mocks are used to write assertions about the way they are used – e.g. if they were called, which arguments were used etc. It is a flagship technique of interaction-based testing – checking how objects under test use their collaborators (other objects).

from unittest import main, TestCase
from unittest.mock import Mock

def foo(some_object, number):
    if number > 2:
        some_object.method(number ** 2)
    else:
        some_object.method(number - 1)

class FooTests(TestCase):
    def test_foo_for_2_calls_method_with_1(self):
        some_object = Mock(method=Mock())

        foo(some_object, 2)

        some_object.method.assert_called_once_with(1)

if __name__ == '__main__': main()

That being said, Python mocks can also be used as stubs – another type of test double that is meant to return canned (hardcoded before) responses. Then, we write assertions about state or value returned from an object under test.

from unittest import main, TestCase
from unittest.mock import Mock


def can_cancel_order(order_id, shipping_system):
    status = shipping_system.get_status(order_id)
    if status in ('SENT', 'DELIVERED'):
        return False
    return True


class CanCancelOrderTest(TestCase):
    def test_can_cancel_order_with_status_sent_false(self):
        order_id = 1
        shipping_system_mock = Mock(get_status=Mock(return_value='SENT'))

        result = can_cancel_order(1, shipping_system_mock)

        self.assertFalse(result)


if __name__ == '__main__': main()

As you can see, we make no assertions about stubbed dependency. Whether we should use mocks or stubs is a whole different story and is beyond the scope of this article.

Note that I used literal 1 for order_id – this could also be classified as test double, but this time called dummy – a value just passed to exercise an object under test but without real influence on the outcome.

The most important takeaway from this part is that mock is used to mimic other objects and we make assertions about how it was used.

Mock – simple examples

To create a mock one needs to instantiate unittest.mock.Mock class. By default, Mock will automagically create missing attributes on-the-fly that will be Mocks. If we call such an attribute, we’re also gonna get Mock.

from unittest.mock import Mock


m = Mock()
# Mocks by default create new attributes out of a thin air
m.not_existing_attribute  # <Mock name='mock.not_existing_attribute' id='140507980563984'>

# Calling Mocks returns by default another Mock instance
m.not_existing_attribute()  # <Mock name='mock.not_existing_attribute()' id='140507980625760'>

# once created, autocreated attribute is kept and we keep on getting it
m.not_existing_attribute()  # <Mock name='mock.not_existing_attribute()' id='140507980625760'>

No matter how cool and magic it looks like, it is rather undesired behaviour in tests. Remember that Mocks are to replace real objects. In order to bring any value, they have to behave like them. Reliance on attribute auto-creation is a misuse of mocks and leads to false-positive tests.

The same goes for returned value – we should not let Mocks leak into further calls. To control it, we use return_value keyword argument:

# We can configure sub-mocks using assignment 
m.totally_legit_method = Mock(return_value='BAZINGA')
m.totally_legit_method()  # 'BAZINGA'

# or do it at creation time
m = Mock(totally_legit_method=Mock(return_value='BAZINGA'))

assert_* methods of Mock (+ unsafe parameter)

Mock instances have a bunch of helpful methods that can be used to write assertions. For example, we can easily assert if mock was called at all:

mock.assert_called()
or if that happened with specific arguments:
assert_called_once_with(argument='bazinga')

Before Python 3.5 that feature in combination with dynamic attributes creation could be very dangerous. If we made a typo in assert_* method name, mock would just happily create a Mock instance on the fly. As a result, we won’t even have an assertion (another false-positive test, yay):

m = Mock()
...
m.assert_called_once_wit(argument='bazinga')  # xD <Mock name='mock.assert_called_once_wit()' id='140258735666560'>

Such a mistake was difficult to spot, but luckily Python3.5 introduced a new keyword to Mock – unsafe with False as default. This makes Mock raise an AttributeError if we try to call a not-existing method starting with “assert” or “assret”:

m.assert_called_once_wit(argument='bazinga')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 640, in __getattr__
    raise AttributeError("Attributes cannot start with 'assert' "
AttributeError: Attributes cannot start with 'assert' or 'assret'

Even though this can be seen as a backwards-incompatible change in Python, it definitely was a good one.

Can I make assertions if a real object was called (not a mock)? – wraps

If for whatever reason you decide to not replace your real object in the test but still want to make assertions if it was used, you can leverage wraps parameter:

from unittest import main, TestCase
from unittest.mock import Mock


class FooBar:
    def foo(self, argument: int) -> int:
        return argument ** 2


def function(foobar: FooBar, number: int) -> int:
    return foobar.foo(number * 2)


class FunctionTests(TestCase):
    def test_function_for_10_calls_foobar_with_20(self):
        # use a real instance, just wrap it
        wrapping_mock = Mock(wraps=FooBar())

        result = function(wrapping_mock, 10)

        wrapping_mock.foo.assert_called_once_with(20)


if __name__ == '__main__': main()

Raising exception from Mock (side_effect)

To make our Mock raise an exception upon call, we use keyword argument side_effect:

m = Mock(side_effect=Exception)
m()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 1075, in __call__
    return self._mock_call(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 1079, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 1134, in _execute_mock_call
    raise effect
Exception

The rule is simple – if a value passed as side_effect is an Exception class or instance, it will get raised. However, it turns out side_effect has more interesting applications…

Returning a few different values from Mock, raising different exceptions

It may happen we will be calling the same Mock several times and we need it to return different values for each call or raise an Exception but only on third call. To achieve this, we pass a list (or another sequence) as side_effect:

m = Mock(side_effect=[1, 2, 3])
m()  # 1
m()  # 2
m()  # 3
m()  # raises StopIteration exception

m = Mock(side_effect=[1, IndexError, 3])
m()  # 1
m()  # raises IndexError
m()  # 3

It is not all yet – side_effect can also accept a callable. It will be called and its result (or exception) visible as if it is coming from a mock:

def callable(arg):
    if arg == 2:
        raise Exception
    return True


m = Mock(side_effect=callable)
m(2)  # raises Exception
m(1)  # returns 1

If we combine this capability with classes defining __call__ method, we can mimic any behaviour we want with side_effect:

from unittest.mock import Mock


class ReturnValeusAllAround:
    def __init__(self, values):
       self._index = 0
       self._values = values

    def __call__(self):
        return_value = self._values[self._index]
        self._index = (self._index + 1) % len(self._values)
        return return_value


m = Mock(side_effect=ReturnValeusAllAround([1, 2, 3]))
m()  # 1
m()  # 2
m()  # 3
m()  # 1
m()  # 2
m()  # ...

If a mock accepts arguments, we need to introduce arguments to __call__.

How to safely mock a class? How to safely mock a function?

It is not hard to create a Mock that can be used in a test, but making it safe and reliable is a very different story. If you think seriously about using mocks, you have to get to know spec and spec_set keyword arguments. They both change the undesired default behaviour of Mock class (creating attributes on-the-fly). Both spec and spec_set accept a class/function you want to mimic. spec will raise AttributeError if you try to access an attribute that is not defined on the class while still letting you set non-existent attributes manually. spec_set forbids that and raises AttributeError.

class A:
    def foo(self):
        pass


spec_mock = Mock(spec=A)
spec_mock.foo()  # returns another Mock instance
spec_mock.bar()  # raises AttributeError
spec_mock.bar = Mock()  # allowed with spec

spec_set_mock = Mock(spec_set=A)
spec_set_mock.foo()  # returns another Mock instance
spec_set_mock.bar = Mock()  # raises AttributeError

spec_set should be your default choice.

How to get even safer mocks with sealing?

Now, even though spec_set seems to be good enough safety measure, it is not always sufficient. Assume there is a class with two or more similar methods and we mock only one of them for the test:

from unittest import main
from unittest.mock import Mock


class FooBar:
    def foo(self):
        pass

    def bar(self):
        pass


foo_bar_mock = Mock(spec_set=FooBar, foo=Mock(return_value='XD'))

Now, if we mock one method (foo in the above example), but an object under test uses unmocked method bar, it will still return a Mock instance.

foo_bar_mock.bar()  # xD^2 <Mock name='mock.bar()' id='140259540846288'>

If we are lucky, our test will fail and we will quickly mock the right method. If it is not our lucky day, we are gonna get false-positive (a passing test for the wrong implementation). I fell into such a situation a few years back. In the end, we created a factory method around Mock called “safe_mock” that will raise an exception if you tried to use an unmocked method. There is no need to do that manually today because Python3.7 has remedy included – sealing mocks:

from unittest import main
from unittest.mock import Mock, seal


class FooBar:
    def foo(self):
        pass

    def bar(self):
        pass


foo_bar_mock = Mock(spec_set=FooBar, foo=Mock(return_value='XD'))
seal(foo_bar_mock)
foo_bar_mock.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 653, in __getattr__
    result = self._get_child_mock(
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py", line 1017, in _get_child_mock
    raise AttributeError(mock_name)
AttributeError: mock.bar

You should also always seal all your mocks for extra security.

Mocking versus patching

We know how to make mocks useful and it all looks fine, but all previous examples showed code where we could easily pass a Mock instance to an object under test. In reality, we (too) often deal with code that looks like this:

# foobar.py
...
foobar = FooBar()

# elsewhere.py
from foobar import foobar_instance

def tested_function(arg: int) -> None:
    foobar_instance.foo(...)

foobar instance is an implicit dependency of tested_function. We still may need to replace it in the test, but now there is no simple way to do so.

Luckily, Python has our back and it comes with unittest.mock.patch. I could show examples of how to do patching but despite years of Python experience I still sometimes get them wrong :(. A colleague of mine showed me an alternative – patch.object – that is much easier to get right:

# foobar.py
from unittest import TestCase
from unittest.mock import Mock, seal, patch


class FooBar:
    def foo(self, arg):
        pass


foobar_instance = FooBar()

# elsewhere.py
from foobar import foobar_instance


def tested_function(arg: int) -> None:
    foobar_instance.foo(arg)

# test_elsewhere.py
from unittest import TestCase
from unittest.mock import Mock, patch

import elsewhere
from elsewhere import tested_function


class FunctionTests(TestCase):
    @patch.object(elsewhere, 'foobar_instance', Mock(foo=Mock(return_value=123)))
    def test_tested_function(self):
        tested_function(1)

        elsewhere.foobar_instance.foo.assert_called_once_with(1)

A recipe is simple – we pass a module where our object under test comes from as the first argument of patch.object and name (string!) of an object being mocked as a second argument.

patch as well as patch.object both can be used as decorators or context managers:

def test_tested_function_with_context_manager(self):
    with patch.object(elsewhere, 'foobar_instance', Mock(foo=Mock(return_value=123))) as foobar_mock:
        tested_function(2)

    foobar_mock.foo.assert_called_once_with(2)

However, consider patching as your last resort for code you do not control. It should be done sparingly. The real problem is that tested_function is untestable. To make it easier you should rather consider refactoring your code to use explicit composition and dependency injection via __init__ arguments:

from foobar import FooBar

# it is much easier to pass mock there
def tested_function(foobar: FooBar, arg: int) -> None:
    foobar.foo(..)

How do I mock or patch requests? How to mock time/datetime? How to mock another popular library?

If you’re struggling to patch a third party library you have no control over, chances are someone has already done it and created a library to help you with that.

For an excellent library for making HTTP calls – requests – a test utility called responses (I admire that name) has been created.

Mocking time/datetime, doing some time-travel, eh? Look no more, use freezegun.

Async Mocks – an honourable mention

If we write asynchronous code, we may need at some point a possibility to conveniently mock coroutines. Since Python3.8, there is AsyncMock class in standard library. If you’re stuck with older python, you can use one of several available libraries.

Summary

Knowing how to write mocks is one thing, but testing effectively is a completely different story. It is beyond the scope of this article, but you will see articles on that matter on that blog as well. 🙂 However, being proficient with Mocks is already a huge step forward. Do not try to patch/hack your code to be able to test – rather make it easily testable.

Always remember about using spec_set and sealing your mocks!

The post How to mock in Python? – (almost) definitive guide appeared first on Breadcrumbs Collector.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...