How to Mock Patch One Function Invoked by Another Function in Python

Mehvish Ashiq Feb 02, 2024
  1. Mock Objects and Their Importance
  2. Python Patch() and Its Uses
How to Mock Patch One Function Invoked by Another Function in Python

Unit tests are essential whenever we write robust code. They help us verify our application logic and inspect all other aspects of the code requirements; however, hurdles like complexity and external dependencies make it hard to write quality test cases.

At this point, unittest.mock, a Python mock library, helps us overcome these obstacles. Today, we will learn about mock objects, their importance, and various example codes using Python patch(), which will temporarily replace the target with the mock objects.

Mock Objects and Their Importance

Mock objects mimic the behavior of real objects in controlled ways, for instance, within the testing environment, where we perform different tests to ensure that code is working as expected. The process of using mock objects is called Mocking; it is a powerful tool to improve the quality of our tests.

Now, the point is, why do we have to go through mocking? What are the fundamental reasons for using it?

Various causes show the importance of using mocking in Python. Let’s have a look at them below:

  1. One primary reason is to improve the behavior of our code by using mock objects. For example, we can use them to test HTTP requests (a request made by a client to the named host, which locates on a server) and ensure whether they cause any intermittent failure or not.
  2. We can replace the actual HTTP requests with the mock objects, which allows us to fake an external service outage and completely successful responses in a predictable way.
  3. Mocking is also useful when it is difficult to test if statements and except blocks. Using mock objects, we can control the execution flow of our code to reach such areas (if and except) and improve code coverage.
  4. Another reason that increases the importance of using Python mock objects is properly understanding how we can use their counterparts in our code.

Python Patch() and Its Uses

The unittest.mock, a mock object library in Python, has a patch() that temporarily replaces the target with the mock object. Here, the target can be a class, a method, or a function.

To use the patch, we need to understand how to identify a target and call the patch() function.

To recognize the target, make sure that the target is importable, then patch the target where it is utilized, not where it is coming from. We can call patch() in three ways; as a decorator for a class/function, as a context manager, or as a manual start/stop.

The target is replaced with the new object when we use patch() as a decorator of a class/function or use patch() in the context manager inside a with statement. In both scenarios, the patch is undone when the with statement or the function exists.

Let’s create a startup code and save it in the addition.py file, which we will import to use patch() as a decorator, context manager, and manual start/stop.

Startup Example Code (saved in addition.py file):

def read_file(filename):
    with open(filename) as file:
        lines = file.readlines()
        return [float(line.strip()) for line in lines]


def calculate_sum(filename):
    numbers = read_file(filename)
    return sum(numbers)


filename = "./test.txt"
calculate_sum(filename)

Content of test.txt:

1
2
3

read_file() takes the filename to read lines and converts every line into float type. It returns a list of these converted numbers, which we save in the numbers variable inside the calculate_sum() function.

Now, the calculate_sum() sums all the numbers in the numbers list and returns it as an output as follows:

OUTPUT:

6.0

Use patch() as a Decorator

Let’s dive in step-by-step to learn the use of patch() as a decorator. Complete source code is given at the end of all steps.

  • Import libraries and modules.
    import unittest
    from unittest.mock import patch
    import addition
    

    First, we import the unittest library, then patch from the unittest.mock module. After that, we import addition, our startup code.

  • Decorate the test_calculate_sum() method.
    @patch('addition.read_file')
    def test_calculate_sum(self, mock_read_file):
        # ....
    

    Next, we decorate the test_calculate_sum() test method using @patch decorator. Here, the target is read_file() function of the addition module.

    The test_calculate_sum() has an additional parameter mock_read_file due to using @patch decorator. This additional parameter is an instance of MagicMock (we can rename mock_read_file to anything that we want).

    Inside the test_calculate_sum(), the patch() replaces addition.read_file() function with mock_read_file object.

  • Assign a list to mock_read_file.return_value.
    mock_read_file.return_value = [1, 2, 3]
    
  • Call calculate_sum() and test it.
    result = addition.calculate_sum("")
    self.assertEqual(result, 6.0)
    

    Now, we can call the calculate_sum() and use assertEqual() to test whether the sum is 6.0 or not.

    Here, we can pass any filename to calculate_sum() function because the mock_read_file object will be invoked instead of addition.read_file() function.

  • Here is the complete source code.

    Example Code (saved in test_sum_patch_decorator.py file)

    import unittest
    from unittest.mock import patch
    import addition
    
    
    class TestSum(unittest.TestCase):
        @patch("addition.read_file")
        def test_calculate_sum(self, mock_read_file):
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6.0)
    

    Run a Test:

    python -m unittest test_sum_patch_decorator -v
    

    Run the test using the above command to get the following output.

    OUTPUT:

    test_calculate_sum (test_sum_patch_decorator.TestSum) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK
    

Use patch() as the Context Manager

Example Code (saved in test_sum_patch_context_manager.py file):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        with patch("addition.read_file") as mock_read_file:
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6)

This code is similar to the last code example where we used patch() as the decorator, except for a few differences discussed here.

Now, we do not have @patch('addition.read_file') line of code while the test_calculate_sum() takes self parameter (which is a default parameter) only.

The with patch('addition.read_file') as mock_read_file means patch addition.read_file() using mock_read_file object in the context manager.

In simple words, we can say that the patch() will replace the addition.read_file() with mock_read_file object within the with block.

Now, run the test using the command below.

python -m unittest test_sum_patch_context_manager -v

OUTPUT:

test_calculate_sum (test_sum_patch_context_manager.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Use patch() to Manually Start/Stop

Example Code (saved in test_sum_patch_manually.py file):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        patcher = patch("addition.read_file")
        mock_read_file = patcher.start()
        mock_read_file.return_value = [1, 2, 3]
        result = addition.calculate_sum("")
        self.assertEqual(result, 6.0)
        patcher.stop()

This code fence does the same as the previous two code examples, but here, we use patch() manually. How? Let’s understand it below.

First, we import the required libraries. Inside the test_calculate_sum(), we call patch() to start a patch with the target read_file() function of the addition module.

Then, we create one mock object for read_file() function. After that, assign the list of numbers to mock_read_file.return_value, call the calculate_sum() and test the results using assertEqual().

Finally, we stop patching by calling the patcher object’s stop() method. Now, run the following command to test.

python -m unittest test_sum_patch_manually -v

OUTPUT:

test_calculate_sum (test_sum_patch_manually.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Mehvish Ashiq avatar Mehvish Ashiq avatar

Mehvish Ashiq is a former Java Programmer and a Data Science enthusiast who leverages her expertise to help others to learn and grow by creating interesting, useful, and reader-friendly content in Computer Programming, Data Science, and Technology.

LinkedIn GitHub Facebook