How to Use Mock in PyTest?

Rate this post

In unit tests, you would want to focus on the code you want to test and avoid dealing with external dependencies that your code might be using. Or, your program might have external dependencies that are not available at the time of the test. In these situations, you can use mocks in your tests.

This article will explain the basic functionality of the mock library in Python and how to use it with pytest using a simple example of an object-oriented program. As you go over the article, you can watch the accompanying video tutorial:

What is Mock in Testing?

In testing, a mock is an object that replaces a dependency. When testing software programs that use external dependencies, you might want to use mocks for the following reasons:

  • You want to isolate the code from the dependencies and focus on the part to test. 
  • The dependencies are not available.
  • The dependencies are available, but it is costly to use them in tests regarding money or time.

In these situations, it is convenient to replace the dependencies with objects that can simulate the behavior of the actual objects, and it is precisely what mocks can offer.

How Does Mock Work in Python?

Python has a built-in mock library called unittest.mock. It has a lot of functionality, but you only need to know the main functionality of the Mock object to get started.

How does the Mock object work?

The mock library has a class called Mock. When you create a Mock object, it is a Mock object. If you call it, you get another Mock object. If you access any attributes or methods of the Mock object, it creates them dynamically and returns a new Mock object. You can configure them to return any value you like.

Open a Python interpreter and import Mock from unittest.mock.

$ python
Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec  7 2020, 12:44:01)
[Clang 12.0.0 (clang-1200.0.32.27)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from unittest.mock import Mock

You can create a mock instance from the Mock class, and you can see that it is an object of the Mock class.

>>> mock = Mock()
>>> type(mock)
<class 'unittest.mock.Mock'>

When you call the Mock object, you get another Mock object.

>>> mock()
<Mock name='mock()' id='4309251120'>

You can create a new attribute a on the fly. It returns a new Mock object by default.

>>> mock.a
<Mock name='mock.a' id='4309251312'>
>>> type(mock.a)
<class 'unittest.mock.Mock'>

You can create another new attribute called b, and it also returns a new Mock object.

>>> mock.b
<Mock name='mock.b' id='4309147168'>
>>> type(mock.b)
<class 'unittest.mock.Mock'>

By default, a Mock object returns another Mock object when you call it.

>>> type(mock.a())
<class 'unittest.mock.Mock'>

But you can use return_value so that the attribute acts as a method returning a specific value, for example, 'A'.

>>> mock.a.return_value = 'A'
>>> mock.a()
'A'

Alternatively, you can use side_effect so that the mock calls a specific function when it is called.

>>> def func(n):
...     return n + 1
...
>>> mock.b.side_effect = func
>>> mock.b(1)
2

You can specify an Error or an Exception in side_effect. For example, you can configure a mock to raise a ZeroDivisionError.

>>> mock.c.side_effect = ZeroDivisionError
>>> mock.c()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 1093, in __call__
    return self._mock_call(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 1152, in _execute_mock_call
    raise effect
ZeroDivisionError

Now you can create a mock and make it behave in whatever way you like.

How Can You Use Mock in Tests?

The Mock class provides various assertion methods to verify the mock object’s behavior. The following example shows that the mock keeps track of the call (a()), and you can check it by using mock_calls.

>>> mock = Mock()
>>> mock.a()
<Mock name='mock.a()' id='4375069216'>
>>> mock.mock_calls
[call.a()]

You can verify whether a specific mock is called or not by using assert_called(), which returns None when the assertion is successful. For example, if you call the method a() of the mock object mock, the assertion mock.a.assert_called() returns None (= pass).

>>> mock.a()
<Mock name='mock.a()' id='4315854064'>
>>> mock.a.assert_called()

If a() has not been called, the assertion raises AssertionError below.

>>> mock = Mock()
>>> mock.a.assert_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 876, in assert_called
    raise AssertionError(msg)
AssertionError: Expected 'a' to have been called.

There are several variations, such as assert_called_once(), assert_called_with(*args, **kwargs), assert_not_called(), etc. to check more specific assert conditions.

You can also check the order of calls. The attribute mock_calls returns all the calls, and assert_has_calls() checks whether the specified (expected) calls match the actual calls or not. 

>>> from unittest.mock import Mock, call
>>> mock = Mock()
>>> mock.a()
<Mock name='mock.a()' id='4346518544'>
>>> mock.b()
<Mock name='mock.b()' id='4346663312'>
>>> mock.a()
<Mock name='mock.a()' id='4346518544'>
>>> mock.mock_calls
[call.a(), call.b(), call.a()]
>>> mock.assert_has_calls([call.a(), call.b(), call.a()])

If the actual call order is different from the expected call order, it will raise an AssertionError.

>>> mock.assert_has_calls([call.a(), call.a(), call.b()])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 944, in assert_has_calls
    raise AssertionError(
AssertionError: Calls not found.
Expected: [call.a(), call.a(), call.b()]
Actual: [call.a(), call.b(), call.a()]

Example of Unit Tests Using Mock in Python

In this section, I will use an example in the article Mocks Aren’t Stubs by Martin Fowler. 

We have two classes, Warehouse and Order. The Warehouse object holds inventories of various products. When we get an Order object, we fill it from a Warehouse object. If the Warehouse object has enough product, the inventory is reduced, and the order is filled. If there is not enough product, the order is not filled. 

Here, let’s assume that we already know the specification of the Warehouse class. So, we can implement the Order class based on the specification as shown below. 

order.py

class Order:

    def __init__(self, product, quantity):
        self._product = product
        self._quantity = quantity
        self._filled = False

    def fill(self, warehouse):
        has_inventry = warehouse.has_inventory(
            self._product, 
            self._quantity
        )
        if has_inventry:
            warehouse.remove(self._product, self._quantity)
            self._filled = True

    def is_filled(self):
        return self._filled

The Order class initializer takes two arguments, product and quantity, which are set to the attributes self._product and self._quantity, respectively. 

The class has an instance method called fill(), which takes a warehouse object. According to the (imaginary) spec, we need to invoke its method has_inventory() with the arguments self._product and self._quantity to check the inventory. 

The method has_inventory() returns True or False, depending on the inventory status. If it has a sufficient amount of the product, it returns True, so the fill() method can call remove() to update the inventory. Then, we can set the attribute self._filled to True, indicating that the order has been filled.

If the inventory does not have enough quantity of the product, has_inventory() returns False, so the self._filled value stays False, indicating that the order is not filled. 

The instance method is_filled() returns the current value of the self._filled attribute.

Now let’s look at how we can run unit tests for this code. We will mock the Warehouse object as we only focus on the Order class. 

How to use Mock in Pytest

As shown below, we can write the first test case to check that the order is filled when the warehouse has enough product. 

test_order.py

from unittest.mock import Mock
from order import Order

def test_order_is_filled():
    
    # setup - data
    order = Order('Talisker', 50)
    warehouse = Mock()

    # setup - expectations
    warehouse.has_inventory.return_value = True
    warehouse.remove.return_value = None

    # exercise
    order.fill(warehouse)
  
    # verify
    assert order.is_filled()

Firstly, we need to import the Mock class from unittest.mock. We also need to import the Order class to test.

The first part of the test is to set up the test data. We can create an order of Talisker with the quantity 50. Then, we need a Warehouse object. As we are not interested in testing the Warehouse class (or we might not have access to the Warehouse class in the test environment), we mock a warehouse object by using Mock to simulate the behavior of a Warehouse object. It means that warehouse is not an actual Warehouse object but behaves like a Warehouse object, and we can control how it behaves.

In the second part, we set the expectations. As we saw earlier, we can create any methods dynamically in a Mock object, so we can create the has_inventory() method and set it to return True, simulating the inventory has enough Talisker. We then create the remove() method, which returns None

In the third part, the order object invokes the fill() method with the warehouse object as an argument. The order object does not know that warehouse is a Mock object, so it simply runs the method as if it were a real warehouse object. 

Finally, we check if the order has been filled or not. In this case, is_filled() should return True because has_inventory() returns True.

You can check that the test passes by running pytest.

$ pytest test_order.py -v
==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/mikio/pytest4
collected 1 item                                            

test_order.py::test_order_is_filled PASSED            [100%]

===================== 1 passed in 0.04s =====================     

We can also add another test case where the warehouse does not have enough inventories.

test_order.py

from unittest.mock import Mock
from order import Order

def test_order_is_filled():
    
    # setup - data
    order = Order('Talisker', 50)
    warehouse = Mock()

    # setup - expectations
    warehouse.has_inventory.return_value = True
    warehouse.remove.return_value = None

    # exercise
    order.fill(warehouse)
  
    # verify
    assert order.is_filled()

def test_order_is_not_filled():
    
    # setup - data
    order = Order('Talisker', 50)
    warehouse = Mock()
  
    # setup - expectations
    warehouse.has_inventory.return_value = False
  
    # exercise
    order.fill(warehouse)

    # verify
    assert not order.is_filled()

In the second test case, the has_inventory() method of the mocked warehouse object returns False, which simulates the inventory has insufficient Talisker. So, the order is not filled, and the is_filled() method should return False. You can check that the second test also passes.

$ pytest test_order.py -v 
==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/mikio/pytest4
collected 2 items                                           

test_order.py::test_order_is_filled PASSED            [ 50%]
test_order.py::test_order_is_not_filled PASSED        [100%]

===================== 2 passed in 0.03s ===================== 

If the example above looks confusing at first glance, you can try changing the return value and see what happens. For example, if you change the return value of has_inventory to True in the second test, the test will fail.

$ pytest test_order.py -v
==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/mikio/pytest4
collected 2 items                                           

test_order.py::test_order_is_filled PASSED            [ 50%]
test_order.py::test_order_is_not_filled FAILED        [100%]

========================= FAILURES ==========================
_________________ test_order_is_not_filled __________________

    def test_order_is_not_filled():
    
        # setup - data
        order = Order('Talisker', 50)
        warehouse = Mock()
    
        # setup - expectations
        warehouse.has_inventory.return_value = True
    
        # exercise
        order.fill(warehouse)
    
        # verify
>       assert not order.is_filled()
E       assert not True
E        +  where True = <bound method Order.is_filled of <order.Order object at 0x10b2481c0>>()
E        +    where <bound method Order.is_filled of <order.Order object at 0x10b2481c0>> = <order.Order object at 0x10b2481c0>.is_filled

test_order.py:33: AssertionError
================== short test summary info ==================
FAILED test_order.py::test_order_is_not_filled - assert no...
================ 1 failed, 1 passed in 0.05s ================

You can see that the is_filled() method returns True. It is because has_inventory returns True, meaning that the inventory has enough Talisker. Hopefully, you can see that you can fully control the behaviour of the dependency (= warehouse) because we are using Mock instead of the real Warehouse object.

How to assert method calls using Mock in pytest 

As we saw earlier, the Mock class has various assertion methods about method calls. So, we can check what methods the order object internally calls.

We can add two additional assert statements at the end of each test, as shown below.

test_order.py

from unittest.mock import Mock, call
from order import Order


def test_order_is_filled():
    
    # setup - data
    order = Order('Talisker', 50)
    warehouse = Mock()

    # setup - expectations
    warehouse.has_inventory.return_value = True
    warehouse.remove.return_value = None

    # exercise
    order.fill(warehouse)
  
    # verify
    assert order.is_filled()
    warehouse.has_inventory.assert_called_with('Talisker', 50)
    warehouse.assert_has_calls(
        [
            call.has_inventory('Talisker', 50), 
            call.remove('Talisker', 50)
        ]
    )
    
    
def test_order_is_not_filled():
    
    # setup - data
    order = Order('Talisker', 50)
    warehouse = Mock()
  
    # setup - expectations
    warehouse.has_inventory.return_value = False
  
    # exercise
    order.fill(warehouse)

    # verify
    assert not order.is_filled()
    warehouse.has_inventory.assert_called_with('Talisker', 50)
    warehouse.remove.assert_not_called()

In the first test, we can add the two assertions:

warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.assert_has_calls(
    [
        call.has_inventory('Talisker', 50), 
        call.remove('Talisker', 50)
    ]
)

The first assert_called_with() method checks that the warehouse object has called the method has_inventory() with the arguments 'Talisker', 50.

The second assert_has_calls() method verifies that the warehouse object has called the two methods (has_inventory('Talisker', 50) and remove('Talisker', 50)) in this order.

We can also add similar assertions to the second test.

warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.remove.assert_not_called()

The first assert_called_with() is the same as the first test, but the second assert_not_called() checks that the warehouse object has not called the remove() method. It is important to have this assertion because we cannot check it just by looking at the output of the is_filled() method.

We can confirm that the tests still pass.

$ pytest test_order.py -v
==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/mikio/pytest4
collected 2 items                                           

test_order.py::test_order_is_filled PASSED            [ 50%]
test_order.py::test_order_is_not_filled PASSED        [100%]

===================== 2 passed in 0.05s =====================

Considerations

Mocking is a powerful tool in testing, but there are some considerations.

Firstly, mocks require maintenance. As we create mocks ourselves based on a specification, any changes to the dependencies will also need to be implemented in the mocks. Otherwise, all unit tests might pass, but the entire software program will not function. If you strictly follow the Open–closed principle, this might not be a big issue, but it would depend on how you develop software.

It leads to the second point. Mocks work better in some software design styles than others. Martin Fowler highlights the differences between testers who use mocks (mockist) and more classical style testers (classicists) in the article Mocks Aren’t Stubs as mentioned above, saying that one of the differences is that mockist prefers the “outside-in approach”:

Once you have your first test running, the expectations on the mocks provide a specification for the next step and a starting point for the tests. You turn each expectation into a test on a collaborator and repeat the process working your way into the system one SUT at a time. This style is also referred to as outside-in, which is a very descriptive name for it. It works well with layered systems. You first start by programming the UI using mock layers underneath. Then you write tests for the lower layer, gradually stepping through the system one layer at a time. This is a very structured and controlled approach, one that many people believe is helpful to guide newcomers to OO and TDD.

In the above example, the expectations in test_order.py would become a specification of the Warehouse class so that we could create tests based on this specification, and so on. You can read the article to get more insights into unit testing, mocking and test-driven development in general

Summary 

In this article, I’ve explained how mocks work in Python by looking at the basic functionality of Mock in the unittest.mock library. It is a handy tool to replace an external dependency and set up tests. Then we looked at how we can use it with pytest by using an example of an object-oriented program.

I hope this article has helped you understand how mocks work and how you can use them with pytest to run tests more effectively.