The Magic of Mocking in Python
According to its official documentation page, mock is a library for testing in Python. It allows you to replace parts of your system under test with mock objects and make assertions about how they have been used.
Before Python 3.3, mock used to be a separate library. Now it's available as unittest.mock from Python 3.3. In this post, we'll discuss three of the coolest magics about mocking in python. These are talked about briefly below:
- Setting return value on function call — One of the cool things about mocking is that you can set the return value of any function call when writing tests. This allows you to test things like varying results of API calls, certain edge cases that might not have been possible to produce, as well as values that are uncertain or gotten randomly. (e.g. random.choice, random.randint, uuid.uuid4 etc).
import uuid def generate_unique_ids(n): return [uuid.uuid4() for _ in range(n)] # Test the generate_unique_ids function import unittest import uuid from mock import patch class TestMockFunctionReturnValue(unittest.TestCase): @patch('uuid.uuid4') def test_mock_return_value(self, uuid_mock): unique_id = uuid.uuid4() uuid_mock.return_value = unique_id generated_ids = generate_unique_ids(1) self.assertEqual([unique_id], generated_ids)
Explanation: We defined a function
generate_unique_ids to generate a number of unique IDs. However, because the return value of
uuid.uuid4 is unpredicatable, we are not able to fully test the behavior of the function.
Since we can use the mock library, it provides us a way to set the return value, we can be sure of what the
generate_unique_ids function returns.
2. Checking that a function was called — mock has an assert method (
assert_called) that helps us test that a function was called. This could be useful when testing that one of many functions called is made if a condition is met or to test functions with unpredictable behavior, like:
# Using Python 3 import random def generate_and_shuffle(n): # Generate list of numbers from 1 up to n and shuffle them numbers = list(range(1,n+1)) random.shuffle(numbers) return numbers # Test that the random.shuffle function call was called import unittest from mock import patch class TestMockFunctionCall(unittest.TestCase): @patch('random.shuffle') def test_mock_function_call(self, random_shuffle_mock): generate_and_shuffle(5) # Assert that random.shuffle was called random_shuffle_mock.assert_called()
Explanation: We defined a function
generate_and_shuffle that generates a list of numbers and shuffles the list. However, testing what the function returns could become difficult due to the unpredictability of the call to random.shuffe.
Mocking saves us in this case. With an assert method
assert_called available on the mock object random_shuffle_mock, we could just simply check that there is a call to the random.shuffle method. It gets more interesting with another magic of mocking discussed next. We could assert that the random.shuffle was called with the expected arguments.
3. Checking that a function was called with specific argument(s) — In magic 2, we only tested that a function was called. However, mock provides another assert_method (
assert_called_with) that goes a step further to test that a function was called with expected arguments.
In the example above, random.shuffle was called to shuffle a list. Since we are quite certain of the value of the list, we can test that the random.shuffle was called with the expected list.
See the example below:
# Using Python 3 import random def generate_and_shuffle(n): # Generate list of numbers from 1 up to n and shuffle them numbers = list(range(1,n+1)) random.shuffle(numbers) return numbers # Test that the function was called with expected arguments import unittest from mock import patch class TestMockFunctionCallWithExpectedArgument(unittest.TestCase): @patch('random.shuffle') def test_mock_function_call_with_expected_arguments(self, random_shuffle_mock): number_to_expected_list_tuple = ((0, ), (1, ), (4, [1,2,3,4])) for number, expected_list in number_to_expected_list_tuple: generate_and_shuffle(number) # Assert that random.shuffle was called random_shuffle_mock.assert_called_with(expected_list)
I have only provided a bit of little information about what mocking could help achieve. You can check out the mock documentation for a detailed overview of the mock library.