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:
- 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.
- 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.
- Mocking is also useful when it is difficult to test
if
statements andexcept
blocks. Using mock objects, we can control the execution flow of our code to reach such areas (if
andexcept
) and improve code coverage. - 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, thenpatch
from theunittest.mock
module. After that, we importaddition,
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 isread_file()
function of theaddition
module.The
test_calculate_sum()
has an additional parametermock_read_file
due to using@patch
decorator. This additional parameter is an instance ofMagicMock
(we can renamemock_read_file
to anything that we want).Inside the
test_calculate_sum()
, thepatch()
replacesaddition.read_file()
function withmock_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 useassertEqual()
to test whether the sum is6.0
or not.Here, we can pass any
filename
tocalculate_sum()
function because themock_read_file
object will be invoked instead ofaddition.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