There are some good references around regarding the Python Mock Object library, the short and simple one and the long and detailed one.
Prerequisite Understanding
- In Python 3.3 and later, the standard library should include unittest.mock, which is everything you need
- Good to have some understanding of python decorator and syntactic sugar (@) and how it works
- Good to have some understanding of pytest fixture and how it works
When to Use
- The Mock object library is often used when testing with pytest
- Often used together with the patch to replace a target function inside the function to test
- The target function is usually some expensive function or some function with an unstable return (like an external API call)
How to Use
Let's say we have a file function.py with the following content:
import random
import time
def compute():
response = expensive_api_call()
addon = unstable_api_call()
return response + addon
def expensive_api_call():
time.sleep(1000)
return 123
def unstable_api_call():
return random.randint(1, 2)
To write a test case, we can have another file, test_function.py with the test function as below:
# Normal Testing
def test_compute():
expected = 124
actual = compute()
assert expected == actual
Now, we already knew we had an expensive function and an unstable API call that might break our test, to avoid that we can mock both functions using the Mock object.
Note:
- when mocking an object (function), we need to mock where the object is imported into
- Various usage examples show the use of mock and patch in different ways. This can be confusing to many. But as long as you understand the principles they should all lead to the same results.
Let's start with one basic way
from unittest.mock import patch
# Note the syntax,
# apply @patch("<target_function>", return_value=<value you want>)
# the <target_function> need to be in str so the patch can parse
# mock_exp indicate a fixture with name mock_exp will be called, which can be
# passed to the patch function. The exact name of this fixture does not matter
# as we not going to use it directly anyway
@patch("functions.expensive_api_call", return_value=123)
def test_compute(mock_exp):
expected = 124
actual = compute()
assert expected == actual
Special Use Case 1 — mock an object as return value
This apply when we want the return_value from the function being mocked to be an object, which can have different attributes to be used. A common one is when we want to mock the subprocess.run function (check out my article about the subprocess module).
from unittest.mock import MagicMock, patch
# This will patch all subprocess.run() functions called inside the
# functions module
@patch("functions.subprocess.run")
def test_subprocess_run(mock_run):
# Create a mock_object using MagicMock()
mock_object = MagicMock()
# Now we can configure the attribute of this mock_object
# example here, we are configuring the value of stdout and stderr
mock_object.configure_mock(
**{
"stdout":"<string you want>",
"stderr":"<some err text>"
}
)
# Next, we just specify the return_value of mock_run to be the mock_object
# everytime subprocess.run() is called inside functions, the mock_object
# will be returned, where its stdout and stderr attributes are specified
mock_run.return_value = mock_object
Special Use Case 2 — have the return value varies depending on input arguments
This is done by supplying a custom function as the side_effect function. This custom function should handle the logic of determining the return value based on input arguments. Example as below
# In functions.py , let say we have
def compute():
response = expensive_api_call(1)
return response
def expensive_api_call(val:int):
time.sleep(1000)
return 123 + val
# Inside the test file, then we have
from unittest.mock import MagicMock, patch
def custom_return(value:int):
# This custom_return function will return values depending on the input
# You can replace the return values to any object(string) based on any
# logic You want
return 123 + val
# Next we supply the above custom_return function as side_effect when patching
@patch("functions.expensive_api_call", side_effect=custom_return)
def test_compute(mock_exp):
expected = 124
actual = compute()
assert expected == actual
Special Use Case 3 — mocking exception thrown
# This is simple way to mock the exception returned by expensive_api_all
# without calling the function, you can use this to test the exception handling
# of the compute() function, you can also replace the Exception with
# any other exception class
@patch("functions.expensive_api_call", side_effect=Exception())
def test_compute(mock_exp):
expected = 124
try:
actual = compute()
except Exception as exp:
assert True
Special Use Case 4 — mocking two or more function
You need to
- Create each pytest fixture for the function that you want to mock
- Take note of the order of the patch function, the fixtures from left to right are applied with the patch function from bottom to top
# In this case, the fixture mock_exp is applied with patch function
# (functions.expensive_api_call), while the fixture mock_unstable is applied
# with patch function (functions.unstable_api_call)
@patch("functions.unstable_api_call", return_value=1)
@patch("functions.expensive_api_call", return_value=123)
def test_compute(mock_exp, mock_unstable):
expected = 124
actual = compute()
assert expected == actual