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?
main
: the module that contains therequest
function.request
: a string of characters containing the name of the function.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!