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 the get_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.

  1. Using decorator (as explained above)
  2. 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

Resources

1 reply

Trackbacks & Pingbacks

  1. […] Python Unit Testing – Mocks […]

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published.