• 10 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 8/30/22

Use Mock Objects to Simulate Object Behavior

Mock objects can simulate an object’s behavior as well as the behavior of constants and functions. Mock objects are therefore an important component when creating unit tests, because they allow us to test our code without testing  the behavior of dependencies.

For dynamic programming languages such as Python, we refer to this as monkey patching. In this context, when an object or method is called, the behavior is modified dynamically so that the value(s) we define within the test can be returned.

We’ve seen in previous chapters that unit testing targets the logic of a single block of code. It’s important to note that during the unit testing process, we are simply testing the internal behavior of a function or method, by providing entry parameters and checking the exit parameters. This means that we don’t need to test the behavior of any functions or methods that are external to our unit of code. The external function will of course have its own unit test.

This is why we want to simulate the behavior of these objects or functions so that we can carry out our testing without having to worry about external dependencies.

But why don’t we just test all parts of the code at the same time, without using a mock? Wouldn’t that be easier?

Well, there is a risk that you’ll fail all the tests in your application by making one small modification to a class. So, it’s better to always isolate the unit of code that you want to test. And this is where mock objects come into play.

Mock objects are mainly used in unit testing, but they are useful in a number of situations:

  • Imitating the response from an API request. We need to be able to run our tests without accessing the Internet. This might seem strange to you, but imagine if you’re working on a train, without internet access. Also, tests need to run very quickly, but a HTTP request can be a very slow operation.

  • Imitating writing to a file. This prevents the creation and deletion of files each time the test is run in our working folder.

  • Working as a team on a project. If you work in a team and you are dependent on another team member, mocks allow you to simulate your colleague’s code. Now, there’s no need to wait until your colleague has finished coding before you can start your testing.

  • Reproducing error scenarios. Sometimes, you’re not able to reproduce a test scenario that generates a particular error result, such as a network issue, an absent database or an API that has temporarily stopped working.

  • Test-Driven Development. You may not have heard of the principle of Test-Driven Development (TDD). To give you an overview, TDD means creating tests before you’ve even written the source code. We’ll take a closer look at this development principle in a later chapter.

There are of course other reasons why we might use mock objects. I’ll leave you to find out about these on your own! In the meantime, I’m going to show you how to create and use a mock using monkeypatch and pytest-mock.

Understand Monkeypatch, a Mocking Solution

Monkeypatch provides a set of methods to mock some of our functionality: 

  • monkeypatch.setattr(obj, name, value, raising=True)  : Modify the behavior of a function or class.

  • monkeypatch.delattr(obj, name, raising=True)  : Delete the function from the test.

  • monkeypatch.setitem(mapping, name, value)  : Modify the elements in a dictionary.

  • monkeypatch.delitem(obj, name, raising=True)  : Delete an element from a dictionary.

  • monkeypatch.setenv(name, value, prepend=False)  : Define an environment variable.

  • monkeypatch.delenv(name, raising=True)  : Delete an environment variable.

Let’s now look at some examples of how these methods can be used.

Monkeypatch a Function

If you want to simulate a function’s behavior while testing, you’ll need to use the  monkeypatch.setattr()  function.

In this example, we’re going to take a function that calls another independent function that sends requests to APIs. There’s no point testing the external function that calls APIs, because this function will have its own unit tests. Also, the function might take a while to run. So, we’ll simulate the behavior of this function using a mock object.

Here’s the  request()  function, which calls some APIs and returns 10. In our scenario, we want to put in a pause of 10 seconds to imitate the time it takes to process the requests. 

import time
 
def request():
    time.sleep(10)
    return 10

Next, we’re going to create the function that will call this mock function, which we’re going to name  main_function() :

def main_function():
    response = request()
    return response

We can now use a unit test to check that our function sends back the response it receives from the  request()  function, but we’re going to mock this function and change the return value: 100 instead of 10.

You’ll also notice that the test lasts a fraction of a second and not the 10 seconds that we’d specified in the  request()  function. This is because when we mock a function, we don’t actually execute the function, we just simulate the function’s return value

Here’s the unit test (we’re going to assume that the two previous functions are held in the same  main.py  file):

import main
from main import main_function
 
def test_main_function(monkeypatch):
 
    def mockreturn():
        return 100
 
    monkeypatch.setattr(main, 'request', mockreturn)
 
    expected_value = 100
    assert main_function() == expected_value

In the above example, we’ve changed the behavior of the  request()  function from the  main  module using the  monkeypatch.setattr(main, 'request', mockreturn)  line of code.

What are these three arguments referring to?

  1. main  : the module that contains the  request  function.

  2. request  : a string of characters containing the name of the function.

  3. mockreturn  : the function that returns the replacement value.

Monkeypatch an Object

This time, we’re going to mock the behavior of a class and simulate its methods.

Let’s consider the following class:

class Player:
    def __init__(self, name, level):
        self.name = name
        self.level = level
 
    def get_info(self):
        infos = {"name" : self.name,
        "level" : self.level}
        return infos

And here’s the function  create_player() , which instantiates the  Player  class and calls the  get_info()  method:

def create_player():
    player = Player("Ranga", 100)
    infos = player.get_info()
    return infos

Now, we want to create the following test that will simulate the return value from the  get_info()  function:

import main
from main import create_player
 
class MockResponse:
 
    @staticmethod
    def get_info():
        return {"name": "test", "level" : 200}
 
def test_create_player(monkeypatch):
 
    def mock_get(*args, **kwargs):
        return MockResponse()
 
    monkeypatch.setattr('main.Player', mock_get)
 
    expected_value = {"name": "test", "level" : 200}
    assert create_player() == expected_value

In actual fact, we haven’t just mocked the  get_info()  method, we’ve actually mocked the whole  Player  class. We first created a  MockResponse  class that holds the full set of methods for the class that we want to mock. And then the  mock_get  function returns one instance of the  MockResponse  class that defines the new behavior of the  get_info()  method.

So then, when the  monkeypatch.setattr('main.Player', mock_get)  is executed, the  Player  instance is replaced by the  MockResponse  instance. This is why the  get_info()  function returns the dictionary  {"name": "test", "level" : 200} .

Use Pytest-Mock for Mocking

Pytest provides a brilliant plugin to manage mocking really easily within our projects. To use it, you first have to install the pytest-mock plugin, using the following command: 

pip install pytest-mock

Using the  mocker  fixture in pytest-mock, you can mock:

  • a constant.

  • a function or method.

  • an object.

Let’s see how to do it.

Mock a Constant

Sometimes, you can’t define a constant without running the whole application. Certain constants are assigned when the application is launched by reading environment variables or simply by reading a configuration file. This means that it’s impossible to know the value of the constant during unit testing.

Let’s take the following function that we’ve created in a file named  circle.py . The function returns the circumference of a circle using the constant  PI :

PI = 3.1415
 
def circumference(radius):
    return 2 * PI * radius

If you want to test your function using a value not equal to the constantPI, you can use the  mocker.patch.object()  function and replace the constant with a different value.

So, we’ll create a test file called  test_circle.py  to test the circumference() function with  PI = 3.14 .

import circle
from circle import circumference
 
def test_should_return_circumference(mocker):
    mocker.patch.object(circle, 'PI', 3.14)
    expected_value = 12.56
    assert circumference(2) == expected_value

 The line  mocker.patch.object(circle, 'PI', 3.14)  lets us change the value of the  PI  constant in the  circle  module to  3.14 . So, we can use this method to test our function with a predefined value of our choice when we create the test.

Mock a Function

Let’s have another look at the example we used to mock the  request  method using monkeypatch.

Here’s the source code:

import time
 
def request():
    time.sleep(10)
    return 10
 
def main_function():
    response = request()
    return response

As before, we’re going to mock the  request  function to make it return the value 100 instead of 10.

This time, we have the following unit test:

import main
from main import main_function
 
def test_main_function(mocker):
    mocker.patch('main.request', return_value=100)
 
    expected_value = 100
    assert main_function() == expected_value

In the above example, we’ve changed the behavior of the  request  function held within the  main  module and we’ve replaced the return value with a value of 100 (return_value = 100 ) using the line  mocker.patch('main.request'  , return_value=100) .

In the screencast below, you’ll see how to mock a class method as if it was just a single function.

Mock an Object

Thinking about classes now, let’s have another look at the source code for the monkeypatch part and see how it differs from pytest-mock.

Here’s the source code:

class Player:
    def __init__(self, name, level):
        self.name = name
        self.level = level
 
    def get_info(self):
        infos = {"name" : self.name,
        "level" : self.level}
        return infos
 
def create_player():
    player = Player("Ranga", 100)
    infos = player.get_info()
    return infos

 With pytest-mock, you need to create the  MockReponse  class, which defines the methods you want to simulate, and you configure the mock using the line  mocker.patch('main.Player', return_value = MockResponse()) . The first argument contains the name of the class that you want to simulate and the second argument contains the instance you want to use to replace it.

We’ll have a test that looks like this:

import main
from main import create_player
 
class MockResponse:
 
    @staticmethod
    def get_info():
        return {"name": "test", "level" : 200}
 
def test_create_player(mocker):
 
    mocker.patch('main.Player', return_value = MockResponse())
 
    expected_value = {"name": "test", "level" : 200}
    assert create_player() == expected_value

Go ahead and try running all these examples in your environment.

Over to You!

Let’s look back at the super-calculator project and add some unit tests for the  Controller  module.

Your Mission:

  • Create a sequence of tests for the  Controller  module using pytest-mock.

Find a suggested solution on GitHub!

Let’s Recap!

  • Mocking enables you to simulate the behavior of a method or an object.

  • Monkeypatch provides the  monkeypatch.setattr()  method, which enables you to change the behavior of functions and objects.

  • The  mocker.patch.object()  method allows you to mock a constant.

  • The  mocker.patch()  method allows you to mock a function or an object.

You now know how to use mocks to fully isolate a unit of code so that you can create your unit tests. We can now move on and see how to build unit tests in a web application, using the Flask framework. Let’s get on with it!

Example of certificate of achievement
Example of certificate of achievement