Post

Meet python-mockito and leave built-in mock & patch behind

Batteries included can give you headache

unittest.mock.[Magic]Mock and unittest.patch are powerful utilities in the standard library that can help us in writing tests. Although it is easy to start using them, there are several pitfalls waiting for unaware beginners. For example, forgetting about optional spec or spec_set can give us green tests for code that will fail in prod immediately. You can find several other examples + solutions in the second half of my other post - How to mock in Python? Almost definitive guide.

Last but not least - vocabulary used in the standard library stands at odds with the general testing nomenclature. This has a negative effect on learning effective testing techniques. Whenever a Pythonista needs to replace a dependency in tests, they use a mock. Generally, we call this type of replacement objectTest Double. Mock is merely one specialized type of a Test Double. What is more, there are limited situations when it's the right Test Double. You can find more details in Robert Martin's post The Little Mocker. Or just stay with me for the rest of this article - I'll guide you through. :) To summarise, if a philosopher Ludwig Wittgenstein was right by saying...

The limits of my language means the limits of my world

...then Pythonistas are missing A LOT by sticking to "mocking".

python-mockito - a modern replacement for Python mock & patch

It is said that experience is the best teacher. However, experience does not have to be our own - if we can learn from others' mistakes, then it's even better. Developers of other programming languages also face the challenges of testing. The library I want to introduce to you - python-mockito - is a port of Java's testing framework with the same name. It's safe by default unlike mock from the standard library. python-mockito has a nice, easy to use API. It also helps you with the maintenance of your tests by being very strict about unexpected behaviours. Plus, it has a pytest integration - pytest-mockito for seamless use and automatic clean up.

Introduction to test double types

I must admit that literature is not 100% consistent on taxonomy of test doubles, but generally accepted definitions are:

  • Dummy - an object required to be passed around (e.g. to __init__) but often is not used at all during test execution
  • Stub - an object returning hardcoded data which was set in advance before test execution
  • Spy - an object recording interactions and exposing API to query it for (e.g. which methods were called and with what arguments)
  • Mock - an object with calls expectations set in advance before test execution
  • Fake - an object that behaves just like a production counterpart, but has a simpler implementation that makes it unusable outside tests (e.g. in-memory storage).

If that's the first time you see test double types and you find it a bit imprecise or overlapping, that's good. Their implementation can be similar at times. What makes a great difference is how they are used during the Assert phase of a test. (Quick reminder - a typical test consists of Arrange - Act - Assert phases).

A great rule of thumb I found recently gives following hints when to use which type:

  • use Dummy when a dependency is expected to remain unused
  • use Stub for read-only dependency
  • use Spy for write-only dependency
  • use Mock for write-only dependency used across a few tests (DRY expectation)
  • use Fake for dependency that's used for both reading and writing.

plus mix features when needed or intentionally break the rules when you have a good reason to do so.

This comes from The Test Double Rule of Thumb article by Matt Parker, linked at the end of this post.

Of course we use test doubles only when we have to. Don't write only unit-tests separately for each class/function, please.

python-mockito versus built-in mock and patch

Installation

I'm using Python3.9 for the following code examples. unitttest.mock is included. To get python-mockito run

pip install mockito pytest-mockito

pytest-mockito will be get handy a bit later ;)

Implementing Dummy

Sometimes Dummy doesn't even require any test double library. When a dependency doesn't really have any effect on the test and/or is not used during execution, we could sometimes pass just always None. If mypy (or other type checker) complains and a dependency is simple to create (e.g. it is an int), we create and pass it.

def test_sends_request_to_3rd_party():
    # setting up spy (ommitted)
    interfacer = ThirdPartyInterfacer(max_returned_results=0)  # "0" is a dummy
    
    interfacer.create_payment(...)

    # spy assertions (ommitted)

If a dependency is an instance of a more complex class, then we can use unittest.mock.Mock + seal or mockito.mock. In the following example, we'll be testing is_healthy method of some Facade. Facades by design can get a bit incohesive and use dependencies only in some methods. Dummy is an ideal choice then:

from logging import Logger

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


class PaymentsFacade:
    def __init__(self, logger: Logger) -> None:
        self._logger = logger

    def is_healthy(self) -> bool:
        # uncomment this line if you want to see error messages
        # self._logger.info("Checking if is healthy!")
        return True


def test_returns_true_for_healthcheck_stdlib():
    logger = Mock(spec_set=Logger)
    seal(logger)
    facade = PaymentsFacade(logger)

    assert facade.is_healthy() is True


def test_returns_true_for_healthcheck_mockito():
    logger = mock(Logger)
    facade = PaymentsFacade(logger)

    assert facade.is_healthy() is True

python-mockito requires less writing and also error message is much better (at least in Python 3.9). Unittest Mock (part of a HUGE stack trace):

        if self._mock_sealed:
            attribute = "." + kw["name"] if "name" in kw else "()"
            mock_name = self._extract_mock_name() + attribute
>           raise AttributeError(mock_name)
E           AttributeError: mock.info  # WTH?

/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py:1017: AttributeError

python-mockito:

self = <dummy.PaymentsFacade object at 0x7fb3880cba00>

    def is_healthy(self) -> bool:
>       self._logger.info("Checking if is healthy!")
E       AttributeError: 'Dummy' has no attribute 'info' configured  # CLEAR

dummy.py:12: AttributeError

Dummies are useful when we know they will not (or should not) be used during the test execution. As a side note, dependencies like logger are rarely problematic in tests and we could also write the same test scenario without using test double at all.

Implementing Stub

With stubs we are only interested in ensuring they will return some pre-programmed data. WE DO NOT EXPLICITLY VERIFY IF THEY WERE CALLED DURING ASSERT. Ideally, we should see if they were used or not purely by looking at the test itself.

In the following example, our PaymentsFacade has a dependancy on PaymentsProvider that is an interfacer to some external API. Obviously, we cannot use the real implementation it in the test. For this particular case, we have a read-only collaboration. Facade asks for payment status and interprets it to tell if the payment is complete.

from enum import Enum

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


class PaymentStatus(Enum):
    AUTHORIZED = 'AUTHORIZED'
    CAPTURED = 'CAPTURED'
    RELEASED = 'RELEASED'



class PaymentsProvider:
    def __init__(self, username: str, password: str) -> None:
        self._auth = (username, password)

    def get_payment_status(self, payment_id: int) -> PaymentStatus:
        # make some requests using auth info
        raise NotImplementedError


class PaymentsFacade:
    def __init__(self, provider: PaymentsProvider) -> None:
        self._provider = provider

    def is_paid(self, payment_id: int) -> None:
        status = self._provider.get_payment_status(payment_id)
        is_paid = status == PaymentStatus.CAPTURED
        return is_paid


def test_returns_true_for_status_captured_stdlib():
    provider = Mock(spec_set=PaymentsProvider)
    provider.get_payment_status = Mock(return_value=PaymentStatus.CAPTURED)
    seal(provider)
    facade = PaymentsFacade(provider)

    assert facade.is_paid(1) is True


def test_returns_true_for_status_captured_mockito(when):
    provider = mock(PaymentsProvider)
    when(provider).get_payment_status(2).thenReturn(PaymentStatus.CAPTURED)
    facade = PaymentsFacade(provider)

    assert facade.is_paid(2) is True

python-mockito gives a test-specific api. when (coming from pytest-mockito) is called on a mock specifying the argument. Next, thenReturn defines what will be returned. Analogously, there is a method thenRaise for raising an exception. Notice a difference (except length) - if we called a mock with an unexpected argument, mockito raises an exception:

def test_returns_true_for_status_captured_mockito(when):
    provider = mock(PaymentsProvider)
    when(provider).get_payment_status(2).thenReturn(PaymentStatus.CAPTURED)
    facade = PaymentsFacade(provider)

    assert facade.is_paid(3) is True  # stub is configured with 2, not 3

# stacktrace
    def is_paid(self, payment_id: int) -> None:
>       status = self._provider.get_payment_status(payment_id)
E       mockito.invocation.InvocationError:
E       Called but not expected:
E
E           get_payment_status(3)
E
E       Stubbed invocations are:
E
E           get_payment_status(2)

stub.py:28: InvocationError

If we don't want this behaviour, we can always use ellipsis:

def test_returns_true_for_status_captured_mockito(when):
    provider = mock(PaymentsProvider)
    when(provider).get_payment_status(...).thenReturn(PaymentStatus.CAPTURED)
    facade = PaymentsFacade(provider)

    assert facade.is_paid(3) is True

If we want to remain safe in every case, we should also use type checker (e.g. mypy).

Digression - patching

when can be also used for patching. Let's assume PaymentsFacade for some reason creates an instance of PaymentsProvider, so we cannot explicitly pass mock into __init__:

class PaymentsFacade:
    def __init__(self) -> None:
        self._provider = PaymentsProvider(
            os.environ["PAYMENTS_USERNAME"],
            os.environ["PAYMENTS_PASSWORD"],
        )

    def is_paid(self, payment_id: int) -> None:
        status = self._provider.get_payment_status(payment_id)
        is_paid = status == PaymentStatus.CAPTURED
        return is_paid

Then, monkey patching is a usual way to go for Pythonistas:

def test_returns_true_for_status_captured_stdlib_patching():
    with patch.object(PaymentsProvider, "get_payment_status", return_value=PaymentStatus.CAPTURED) as mock:
        seal(mock)
        facade = PaymentsFacade()

        assert facade.is_paid(1) is True


def test_returns_true_for_status_captured_mockito_patching(when):
    when(PaymentsProvider).get_payment_status(...).thenReturn(
        PaymentStatus.CAPTURED
    )
    facade = PaymentsFacade()

    assert facade.is_paid(3) is True

python-mockito implementation is even shorter with patching than without it. But do not treat this as an invitation for patching :) An important note - context manager with patch.object makes sure there is a cleanup. For pytest, I strongly recommend using fixtures provided by pytest-mockito. They will do cleanup automatically for you, Otherwise, one would have to call function mockito.unstub manually. More details in the documentation of pytest-mockito and python-mockito. Documentation of python-mockito states there is also a way to use it with context managers, but personally I've never done so.

Monkey patching is dubious practice at best - especially if done on unstable interfaces. It should be avoided because it tightly couples tests with the implementation. It can be your last resort, though. The frequent need for patching in tests is a strong indicator of untestable design or poor test or both.

Digression - pytest integration

For daily use with the standard library mocks, there is a lib called pytest-mock. It provides mocker fixture for easy patching and automatic cleanup. The outcome is similar to pytest-mockito.

Implementing Spy

Now, let's consider a scenario of starting a new payment. PaymentsFacade calls PaymentsProvider after validating input and converting money amount to conform to API's expectation.

from dataclasses import dataclass
from decimal import Decimal

from unittest.mock import Mock, seal
from mockito import mock, verify


@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __post_init__(self) -> None:
        if self.amount < 0:
            raise ValueError("Money amount cannot be negative!")


class PaymentsProvider:
    def __init__(self, username: str, password: str) -> None:
        self._auth = (username, password)

    def start_new_payment(self, card_token: str, amount: int) -> None:
        raise NotImplementedError


class PaymentsFacade:
    def __init__(self, provider: PaymentsProvider) -> None:
        self._provider = provider

    def init_new_payment(self, card_token: str, money: Money) -> None:
        assert money.currency == "USD", "Only USD are currently supported"
        amount_in_smallest_units = int(money.amount * 100)
        self._provider.start_new_payment(card_token, amount_in_smallest_units)


def test_calls_provider_with_799_cents_stdlib():
    provider = Mock(spec_set=PaymentsProvider)
    provider.start_new_payment = Mock(return_value=None)
    seal(provider)
    facade = PaymentsFacade(provider)

    facade.init_new_payment("nonsense", Money(Decimal(7.99), "USD"))

    provider.start_new_payment.assert_called_once_with("nonsense", 799)


def test_calls_provider_with_1099_cents_mockito(when):
    provider = mock(PaymentsProvider)
    when(provider).start_new_payment(...).thenReturn(None)
    facade = PaymentsFacade(provider)

    facade.init_new_payment("nonsense", Money(Decimal(10.99), "USD"))

    verify(provider).start_new_payment("nonsense", 1099)

Here, a major difference between unittest.mock and mockito is that the latter:

  • lets us specify input arguments (not shown here, but present in previous examples)
  • (provided input arguments were specified) fails if there are any additional, unexpected interactions.

The second behaviour is added by pytest-mockito that apart from calling unstub automatically, it also calls verifyNoUnwantedInvocations.

Implementing Mock

Let's consider identical test scenario as for Spy - but this time assume we have some duplication in verification and want to refactor Spy into Mock. Now, the funniest part - it turns out that standard library that has only classes called "Mock" does not really make it any easier to create mocks as understood by literature. On the other hand, it's such a simple thing that we can do it by hand without any harm. To make this duel even, I'll use pytest fixtures for both:

@pytest.fixture()
def stdlib_provider():
    provider = Mock(spec_set=PaymentsProvider)
    provider.start_new_payment = Mock(return_value=None)
    seal(provider)
    yield provider
    provider.start_new_payment.assert_called_once_with("nonsense", 799)


def test_returns_none_for_new_payment_stdlib(stdlib_provider):
    facade = PaymentsFacade(stdlib_provider)

    result = facade.init_new_payment("nonsense", Money(Decimal(7.99), "USD"))

    assert result is None


@pytest.fixture()
def mockito_provider(expect):
    provider = mock(PaymentsProvider)
    expect(provider).start_new_payment("nonsense", 1099)
    return provider


def test_returns_none_for_new_payment_mockito(mockito_provider):
    facade = PaymentsFacade(mockito_provider)

    result = facade.init_new_payment("nonsense", Money(Decimal(10.99), "USD"))

    assert result is None

expect will also call verifyUnwantedInteractions to make sure there are no unexpected calls.

Implementing Fake

For Fakes, there are no shortcuts or libraries. We are better off writing them manually. You can find an example here - InMemoryAuctionsRepository . It is meant to be a test double for a real implementation that uses a relational database.

Summary

Initially this blog post was meant to be only about the tool, but I couldn't resist squeezing in some general advice about testing techniques. ;)

While python-mockito does not solve an issue with calling every test double a mock, it definitely deserves attention. Test doubles created with it require less code and are by default much more secure and strict than those using unittest.mock. Regarding cons, camelCasing can be a little distracting at first, but this is not a huge issue. The most important thing is that safety we get out-of-the-box with python-mockito has been being added to python standard library over several versions and is not as convenient.

I strongly recommend to read python-mockito's documentation and try it out!

Further reading

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.