Python Unit Testing – Using Mocks
Writing unit tests for programs that have external dependencies is tricky. One approach is mocking external dependencies. For example, if your program is writing to a MySQL database, you may not want to actually write to the database when you run unit tests. Instead, you can use a mock to simulate this operation.
Python’s unittest.mock
allows you to replace parts of your program with mock objects and test various conditions. In other words, you can replace functions, classes, or objects in your program with mock objects, which are instances of Mock class.
Here is an example to understand the concept.
Function Mocks
Let’s start with a function which makes an API call to determine the weather basted latitude and longitude.
import requests
def get_weather_status(lat, lng):
response = requests.get(f"https://api.weather.gov/points/{lat},{lng}")
if response.status_code == 200:
return response.json()
else:
return None
The above function is called by check_weather
and prints a status message.
def check_weather():
if get_weather(39.7456,-97.0892):
return "Got weather information"
else:
return "Can't get weather information"
What are the different ways we can unit test the check_weather()
function? The get_weather_status()
could return a proper value or None. It could also timeout. Since we are interested to test the check_weather
function, we just need to simulate those conditions. This is where mocks are useful.
We can mock a successful condition where the get_weather_status()
returns a valid JSON.
from unittest import mock
@patch('__main__.get_weather_status')
def mock_check_weather(mock_obj):
mock_obj.return_value = { "properties": "Link"}
print(check_weather)
How does this work?
- First, we are using a decorator called “patch”, which specifies the function that needs to be mocked. In this example, we are mocking the
get_weather_status
function. - Right after the decorator, a function is defined. The name of this function could be anything. By default, a Mock object is passed to this function. More on the Mock object later.
- Inside the
mock_check_weather
function, we are replacing the return_value of the mock object with a value that we expect from theget_weather_status
() function. - The
check_weather
function is invoked from the mock_check_weather function.
The invocation of the mock_check_weather()
will return the following:
>> mock_get_weather()
Got weather information
Effectively, inside the mock_check_weather
function, the get_weather
has been mocked and made to return a value that we expect to see. In order to understand the above code, we need to know more about how the Mock class works.
Mock Object
Unittest.mock
provides the base class for mocking objects. This class is very flexible and you can use it in different ways. To start, instantiate the Mock class.
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock
<Mock id='140499790390080'>
Since mock has to be flexible, it uses the lazy loading concept where attributes and methods are created when you access them.
>>> mock.my_attribute
<Mock name='mock.my_attribute' id='140499790408624'>
>>> mock.my_method()
<Mock name='mock.my_method()' id='140500060927744'>
You can also assign value to an attribute in the Mock by:
- Assigning directly
- Using configure_mock method
- Passing arguments while instantiating a Mock object
>>> m = Mock()
>>> m.my_key = "my_value"
>>> m.configure_mock(my_key="my_value")
You can create a Mock object and add methods to it by simply calling those methods.
>>> car = Mock()
>>> car.model("Tesla")
<Mock name='mock.model()' id='140500060928416'>
>>> car.year("2021")
<Mock name='mock.year()' id='140500060913280'>
Assertions
Mock
object contains information about how the object was used. You can see if you called a method, how you called a method, the number of times a method was called etc.
Here are some examples of using assertions:
>>> car = Mock()
>>> car.model("Tesla")
<Mock name='mock.model()' id='140500060928416'>
>>> car.model.assert_called()
>>> car.model.assert_called_once()
>>> car.model.assert_called_with('Tesla')
>>> car.model.assert_called_with('Honda')
AssertionError: expected call not found.
Expected: model('Honda')
Actual: model('Tesla')
>>> car.model('BMW')
<Mock name='mock.model()' id='140500060928416'>
>>> car.model.assert_called_once()
AssertionError: Expected 'model' to have been called once. Called 2 times
Calls: [call('Tesla'), call('BMW')].
>>> car.model.assert_called_with('BMW')
Return values
To change what is returned when a Mock object is called like function, you can just set the return_value attribute.
from unittest import mock
@patch('__main__.get_weather_status')
def mock_check_weather(mock_obj):
mock_obj.return_value = { "properties": "Link"}
print(check_weather)
Side Effects
If you want to do more than just changing the return value, set side-effect. It allows you to do additional operations like printing to a log, making HTTP calls, and raising exceptions.
Let’s see how we can use side effect to raise an exception from a mocked function.
import requests
def get_weather_status(lat, lng):
response = requests.get(f"https://api.weather.gov/points/{lat},{lng}")
if response.status_code == 200:
return response.json()
else:
return None
requests = Mock()
def test_get_weather_timeout():
requets.get.side_effect = Timeout
with self.assertRaises(Timeout):
get_weather_status()
>> test_get_weather_timeout()
Here is yet another example for mocking the entire response object for a HTTP call.
import requests
def get_weather_status(lat, lng):
response = requests.get(f"https://api.weather.gov/points/{lat},{lng}")
if response.status_code == 200:
return response.json()
else:
return None
requests = Mock()
def test_get_weather_timeout():
respose = Mock()
response.status_code = 200
response.json.return_value = {
}
return response
Mock Vs MagicMock
MagicMock is a Mock variant which allows mocking Python’s magic methods like__str__ and __add__.
from unittest.mock import Mock
>>> m = MagicMock()
>>> m.__str__.return_value = "mine"
>>> str(m)
'mine'
Patching
The idea of patching is to temporarily point an object to another one. It is import to know where to patch to avoid any surprises. There are a couple of ways to use patch.
- Using decorator (as explained above)
- Mocking a code block
with patch('__main__.get_weather_status') as my_mock:
...
Patching Vs Mock
The process of replacing a dependency is called patching, The replacement is a mock, which is an instance of Mock class.
Patching functions
Please see the check_weather() example above.
Patching classes
Just like functions, we can also mock classes.
class HelloWorld(object):
@classmethod
def greet(self):
return "Good morning"
def say(self):
return "Hello World"
Use patch to mock the entire class.
@mock.patch("__main__.HelloWorld")
def mock_hello_world(mock_obj):
print(mock_obj)
mock_obj.greet.return_value = "mocked Hello World"
print(HelloWorld.greet())
# invoke mock_hello_world
>>> mock_hello_world()
<MagicMock name='HelloWorld' id='140186129047360'>
mocked Hello World
Mocking a class method is different than an instance method.
# mock class method
MagicMockObject.class_method_name.return_value = "New value"
# mock instance method
MagicMethodObject.return_value.instance_method_name.return_value = "An instance return value"
Example of changing an instance method.
@mock.patch("__main__.HelloWorld")
def mock_hello_world(mock_obj):
mock_obj.return_value.say.return_value = "Hello Friend"
obj = HelloWorld()
print(obj.say())
>>> mock_hello_world()
Hello Friend
Patching an Object
If you have class and want to mock just a method and not the entire class, you can use @patch.object.
from unittest import mock
class HelloWorld(object):
@classmethod
def greet(self):
return "Good morning"
@classmethod
def say(self):
return "Hello World"
@mock.patch.object(HelloWorld, 'greet')
def mock_hello_world(mock_obj):
mock_obj.return_value = "Good Evening"
print(HelloWorld.greet())
print(HelloWorld.say())
>>> mock_hello_world()
Good Evening
Hello World
The greet()
method is mocked while say()
method remains the same.
Mocking Exception
In some cases, you may want to simulate an exception which can be done using side_effect.
>>> mock = Mock()
>>> mock.side_effect = Exception("Hell Yeah")
>>> mock()
Exception: Hell Yeah
Trackbacks & Pingbacks
[…] Python Unit Testing – Mocks […]
Leave a Reply
Want to join the discussion?Feel free to contribute!