Mocks and monkeypatching in python

Posted on Sun 24 April 2016

Hello, in today's post I will look onto essential part of testing- mocks.

First of all, what I want to accomplish here is to give you basic examples of how to mock data using two tools: mock and pytest monkeypatch.

Why bother mocking?

Some of the parts of our application may have dependencies for other libraries or objects. To isolate behaviour of our parts we need to substitue external dependencies. Here comes the mocking. We mock external API to have certain behaviours such as proper return values that we previously defined.

Mocking function

Let's say we have module called

def square(value):
    return value ** 2

def cube(value):
    return value ** 3

def main(value):
    return square(value) + cube(value)

Then let's look how these functions are mocked using mock library:

 1  try:
 2      import mock
 3  except ImportError:
 4      from unittest import mock
 6  import unittest
 8  from function import square, main
11  class TestNotMockedFunction(unittest.TestCase):
13      @mock.patch('__main__.square', return_value=1)
14      def test_function(self, mocked_square):
15          # because you need to patch in exact place where function that has to be mocked is called
16          self.assertEquals(square(5), 1)
18      @mock.patch('function.square')
19      @mock.patch('function.cube')
20      def test_main_function(self, mocked_square, mocked_cube):
21          # underling function are mocks so calling main(5) will return mock
22          mocked_square.return_value = 1
23          mocked_cube.return_value = 0
24          self.assertEquals(main(5), 1)
25          mocked_square.assert_called_once_with(5)
26          mocked_cube.assert_called_once_with(5)
29  if __name__ == '__main__':
30      unittest.main()

What is happening here? Lines 1-4 are for making this code compatible between python 2 and 3. In python 3 mock is part of standard library whereas in python 2 you need to install by pip install mock.

In line 13 I patched the square function. But you have to remember to patch it in the same place you use it. For instance, I'm calling square(5) in test itself so I need to patch it in __main__. This is the case if I'm running this by python tests/ If I'm using pytest for that I need to patch it like test_function.square.

In lines 18-19, I patch square and cube functions in their module because they are used in main function. The last two asserts come from mock library and are for making sure that mock was called with proper values.

The same can be accomplished using mokeypatching for py.test:

from function import square, main

def test_function(monkeypatch):
    monkeypatch.setattr("test_function_pytest.square", lambda x: 1)
    assert square(5) == 1

def test_main_function(monkeypatch):
    monkeypatch.setattr('function.square', lambda x: 1)
    monkeypatch.setattr('function.cube', lambda x: 0)
    assert main(5) == 1

As you can see I'm using monkeypatch.setattr for setting up return value for given functions. I'm still need to monkeypatch it in proper place: test_function_pytest and function.

Mocking classes

I have module called square:

import math

class Square(object):
     def __init__(radius):
         self.radius = radius

     def calculate_area(self):
         return math.sqrt(self.radius) * math.pi

And mocks using standard lib:

 1 try:
 2     import mock
 3 except ImportError:
 4     from unittest import mock
 6 import unittest
 8 from square import Square
11 class TestClass(unittest.TestCase):
13     @mock.patch('__main__.Square') # depends in witch from is run
14     def test_mocking_instance(self, mocked_instance):
15         mocked_instance = mocked_instance.return_value
16         mocked_instance.calculate_area.return_value = 1
17         sq = Square(100)
18         self.assertEquals(sq.calculate_area(), 1)
21     def test_mocking_classes(self):
22         sq = Square
23         sq.calculate_area = mock.MagicMock(return_value=1)
24         self.assertEquals(sq.calculate_area(), 1)
26     @mock.patch.object(Square, 'calculate_area')
27     def test_mocking_class_methods(self, mocked_method):
28         mocked_method.return_value = 20
29         self.assertEquals(Square.calculate_area(), 20)
31 if __name__ == '__main__':
32     unittest.main()

At line 13 I patch class Square (again be aware if you run this test using pytest or standard way). Lines 15 and 16 presents mocking instance; at first mocked_instance is mock object which by default returns another mock and to these mock.calculate_area I add return_value 1. In line 23 I'm using MagicMock which is normal mock class except it also retrieves magic methods from given object. Lastly I use patch.object to mock method in Square class.

The same using pytest:

    from mock import MagicMock
except ImportError:
    from unittest.mock import MagicMock

from square import Square

def test_mocking_class_methods(monkeypatch):
    monkeypatch.setattr('test_class_pytest.Square.calculate_area', lambda: 1)
    assert Square.calculate_area() ==  1

def test_mocking_classes(monkeypatch):
    monkeypatch.setattr('test_class_pytest.Square', MagicMock(Square))
    sq = Square
    sq.calculate_area.return_value = 1
    assert sq.calculate_area() ==  1

The issue here is with test_mocking_class_methods which works well in python 3 but not in python 2. Right now I don't have clear answer to this so if you can help I appreciate this!

All examples can be found under this repo.

tags: python, testing,

Comments !