23 Mar 2020 · Software Engineering

    Mocks and Monkeypatching in Python

    5 min read
    Contents

    Post originally published on http://krzysztofzuraw.com/. Republished with author’s permission.

    Introduction

    In this post I will look into the 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 the behaviour of our parts, we need to substitute external dependencies. Here comes the mocking. We mock an external API to check certain behaviours, such as proper return values, that we previously defined.

    Mocking function

    Let’s say we have a module called function.py:

    def square(value):
        return value ** 2
    
    def cube(value): 
        return value ** 3
    
    def main(value): 
        return square(value) + cube(value)

    Then let’s see how these functions are mocked using the mock library:

        try:
            import mock
        except ImportError:
            from unittest import mock
    
        import unittest
    
        from function import square, main
    
    
        class TestNotMockedFunction(unittest.TestCase):
    
            @mock.patch('__main__.square', return_value=1)
            def test_function(self, mocked_square):
                # because you need to patch in exact place where function that has to be mocked is called
                self.assertEquals(square(5), 1)
    
            @mock.patch('function.square')
            @mock.patch('function.cube')
            def test_main_function(self, mocked_square, mocked_cube):
                # underling function are mocks so calling main(5) will return mock
                mocked_square.return_value = 1
                mocked_cube.return_value = 0
                self.assertEquals(main(5), 1)
                mocked_square.assert_called_once_with(5)
                mocked_cube.assert_called_once_with(5)
            
    
        if __name__ == '__main__':
            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 the standard library, whereas in Python 2 you need to install it by pip install mock.

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

    In lines 18-19, I patch the square and cube functions in their module because they are used in the main function. The last two asserts come from the mock library, and are there to make 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 a return value for given functions. I still need to monkeypatch it in proper places — test_function_pytest and function.

    Mocking classes

    I have a 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:

    try: 
        import mock 
    except ImportError: 
        from unittest import mock
    
    import unittest
    
    from square import Square
    
    class TestClass(unittest.TestCase):
    
           @mock.patch('__main__.Square') # depends in witch from is run
           def test_mocking_instance(self, mocked_instance):
               mocked_instance = mocked_instance.return_value
               mocked_instance.calculate_area.return_value = 1
               sq = Square(100)
               self.assertEquals(sq.calculate_area(), 1)
    
    
           def test_mocking_classes(self):
               sq = Square
               sq.calculate_area = mock.MagicMock(return_value=1)
               self.assertEquals(sq.calculate_area(), 1)
    
           @mock.patch.object(Square, 'calculate_area')
           def test_mocking_class_methods(self, mocked_method):
               mocked_method.return_value = 20
               self.assertEquals(Square.calculate_area(), 20)
    
    if __name__ == ‘__main__’:
        unittest.main()

    At line 13, I patch the class Square. Lines 15 and 16 present a mocking instance. mocked_instance is a mock object which returns another mock by default, and to these mock.calculate_area I add return_value 1. In line 23, I’m using MagicMock, which is a normal mock class, except in that it also retrieves magic methods from the given object. Lastly, I use patch.object to mock the method in the Square class.

    The same using pytest:

    try: 
        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.

    All examples can be found in this repo.

    If you have any questions and comments, feel free to leave them in the section below.

    References:

    One thought on “Mocks and Monkeypatching in Python

    1. I feel that this article assumes that the reader has more knowledge of monkeypatching than it ought to given it’s introductory level. The author uses the phrase ” I still need to monkeypatch it in proper places — test_function_pytest and function.” Which assumes the reader understands what it means to “monkeypatch” without explaining in any further detail.

      I chose this article hoping for help with understanding that very concept and have left without any further knowledge on the subject.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Python/Django developer, blogger at krzysztofzuraw.com. In his free time, he’s a toastmaster & street workout man. You can find him on Twitter @krzysztof_zuraw.