Python Mocking 101: Fake it before you make it
10 février 2018
0 minutes de lectureEditor's note
This blog originally appeared on fugue.co. Fugue joined Snyk in 2022 and is a key component of Snyk IaC.
FugureWelcome to a guide to the basics of mocking in Python. It was born out of my need to test some code that used a lot of network services and my experience with GoMock, which showed me how powerful mocking can be when done correctly (thanks, Tyler). I'll begin with a philosophical discussion about mocking because good mocking requires a different mindset than good development. Development is about making things, while mocking is about faking things. This may seem obvious, but the "faking it" aspect of mocking tests runs deep, and understanding this completely changes how one looks at testing. After that, we'll look into the mocking tools that Python provides, and then we'll finish up with a full example. Learn more about testing code for Python security with our cheat sheet.
Mocking can be difficult to understand. When I'm testing code that I've written, I want to see whether the code does what it's supposed to do from end-to-end. I usually start thinking about a functional, integrated test, where I enter realistic input and get realistic output. I access every real system that my code uses to make sure the interactions between those systems are working properly, using real objects and real API calls. While these kinds of tests are essential to verify that complex systems are interworking well, they are not what we want from unit tests.
Unit tests are about testing the outermost layer of the code. Integration tests are necessary, but the automated unit tests we run should not reach that depth of systems interaction. This means that any API calls in the function we're testing can and should be mocked out. We should replace any nontrivial API call or object creation with a mock call or object. This allows us to avoid unnecessary resource usage, simplify the instantiation of our tests, and reduce their running time. Think of testing a function that accesses an external HTTP API. Rather than ensuring that a test server is available to send the correct responses, we can mock the HTTP library and replace all the HTTP calls with mock calls. This reduces test complexity and dependencies, and gives us precise control over what the HTTP library returns, which may be difficult to accomplish otherwise.
What do we mean by mocking?
The term mocking is thrown around a lot, but this document uses the following definition:
"The replacement of one or more function calls or objects with mock calls or objects"
A mock function call returns a predefined value immediately, without doing any work. A mock object's attributes and methods are similarly defined entirely in the test, without creating the real object or doing any work. The fact that the writer of the test can define the return values of each function call gives him or her a tremendous amount of power when testing, but it also means that s/he needs to do some foundational work to get everything set up properly.
In Python, mocking is accomplished through the unittest.mock
module. The module contains a number of useful classes and functions, the most important of which are the patch
function (as decorator and context manager) and the MagicMock
class. Mocking in Python is largely accomplished through the use of these two powerful components.
What do we NOT mean by mocking?
Developers use a lot of "mock" objects or modules, which are fully functional local replacements for networked services and APIs. For example, the moto
library is a mock boto
library that captures all boto
API calls and processes them locally. While these mocks allow developers to test external APIs locally, they still require the creation of real objects. This is not the kind of mocking covered in this document. This document is specifically about using MagicMock
objects to fully manage the control flow of the function under test, which allows for easy testing of failures and exception handling.
How do we mock in Python?
Mocking in Python is done by using patch
to hijack an API function or object creation call. When patch
intercepts a call, it returns a MagicMock
object by default. By setting properties on the MagicMock
object, you can mock the API call to return any value you want or raise an Exception
.
The overall procedure is as follows:
Write the test as if you were using real external APIs.
In the function under test, determine which API calls need to be mocked out; this should be a small number.
In the test function, patch the API calls.
Set up the
MagicMock
object responses.Run your test.
If your test passes, you're done. If not, you might have an error in the function under test, or you might have set up your MagicMock
response incorrectly. Next, we'll go into more detail about the tools that you use to create and configure mocks.
patch
import unittest
from unittest.mock import patch
patch
can be used as a decorator to the test function, taking a string naming the function that will be patched as an argument. In order for patch
to locate the function to be patched, it must be specified using its fully qualified name, which may not be what you expect. If a class is imported using a from module import ClassA
statement, ClassA
becomes part of the namespace of the module into which it is imported.
For example, if a class is imported in the module my_module.py
as follows:
[in my_module.py]
from module import ClassA
It must be patched as @patch(my_module.ClassA)
, rather than @patch(module.ClassA)
, due to the semantics of the from ... import ...
statement, which imports classes and functions into the current namespace.
Typically patch
is used to patch an external API call or any other time- or resource-intensive function call or object creation. You should only be patching a few callables per test. If you find yourself trying patch
more than a handful of times, consider refactoring your test or the function you're testing.
Using the patch
decorator will automatically send a positional argument to the function you're decorating (i.e., your test function). When patching multiple functions, the decorator closest to the function being decorated is called first, so it will create the first positional argument.
@patch('module.ClassB')
@patch('module.functionA')
def test_some_func(self, mock_A, mock_B):
...
By default, these arguments are instances of MagicMock
, which is unittest.mock
's default mocking object. You can define the behavior of the patched function by setting attributes on the returned MagicMock
instance.
MagicMock
MagicMock
objects provide a simple mocking interface that allows you to set the return value or other behavior of the function or object creation call that you patched. This allows you to fully define the behavior of the call and avoid creating real objects, which can be onerous. For example, if we're patching a call to requests.get
, an HTTP library call, we can define a response to that call that will be returned when the API call is made in the function under test, rather than ensuring that a test server is available to return the desired response.
The two most important attributes of a MagicMock
instance are return_value
and side_effect
, both of which allow us to define the return behavior of the patched call.
return_value
The return_value
attribute on the MagicMock
instance passed into your test function allows you to choose what the patched callable returns. In most cases, you'll want to return a mock version of what the callable would normally return. This can be JSON, an iterable, a value, an instance of the real response object, a MagicMock
pretending to be the response object, or just about anything else. When patching objects, the patched call is the object creation call, so the return_value
of the MagicMock
should be a mock object, which could be another MagicMock
.
If the code you're testing is Pythonic and does duck typing rather than explicit typing, using a MagicMock
as a response object can be convenient. Rather than going through the trouble of creating a real instance of a class, you can define arbitrary attribute key-value pairs in the MagicMock
constructor and they will be automatically applied to the instance.
[in test_my_module]
@patch('external_module.api_call')
def test_some_func(self, mock_api_call):
mock_api_call.return_value = MagicMock(status_code=200,response=json.dumps({'key':'value'}))
my_module.some_func()
[in my_module]import external_module
def some_func():
response = external_module.api_call()
#normally returns a Response object, but now returns a MagicMock
#response == mock_api_call.return_value == MagicMock(status_code=200, response=json.dumps({'key':'value'}))
Note that the argument passed to test_some_func
, i.e., mock_api_call
, is a MagicMock
and we are setting return_value
to another MagicMock
. When mocking, everything is a MagicMock
.
Speccing a MagicMock
While a MagicMock
’s flexibility is convenient for quickly mocking classes with complex requirements, it can also be a downside. By default, MagicMock
s act like they have any attribute, even attributes that you don’t want them to have. In the example above, we return a MagicMock
object instead of a Response
object. However, say we had made a mistake in the patch
call and patched a function that was supposed to return a Request
object instead of a Response
object. The MagicMock
we return will still act like it has all of the attributes of the Request
object, even though we meant for it to model a Response
object. This can lead to confusing testing errors and incorrect test behavior.
The solution to this is to spec
the MagicMock
when creating it, using the spec
keyword argument: MagicMock(spec=Response)
. This creates a MagicMock
that will only allow access to attributes and methods that are in the class from which the MagicMock
is specced. Attempting to access an attribute not in the originating object will raise an AttributeError
, just like the real object would.
A simple example is:
m = MagicMock()m.foo()
#no error raised
# Response objects have a status_code attributem = MagicMock(spec=Response, status_code=200, response=json.dumps({‘key’:’value’}))m.foo()
#raises AttributeErrorm.status_code #no error raised
side_effect
Sometimes you'll want to test that your function correctly handles an exception, or that multiple calls of the function you're patching are handled correctly. You can do that using side_effect
. Setting side_effect
to an exception raises that exception immediately when the patched function is called.
Setting side_effect
to an iterable will return the next item from the iterable each time the patched function is called. Setting side_effect
to any other value will return that value.
[in test_my_module]
@patch('external_module.api_call')
def test_some_func(self, mock_api_call):
mock_api_call.side_effect = SomeException()
my_module.some_func()[in my_module]def some_func():
try:
external_module.api_call()
except SomeException:
print(“SomeException caught!”)
# this code is executed
except SomeOtherException:
print(“SomeOtherException caught!”)
# not executed[in test_my_module]
@patch('external_module.api_call')
def test_some_func(self, mock_api_call):
mock_api_call.side_effect = [0, 1]
my_module.some_func()[in my_module]
def some_func():
rv0 = external_module.api_call()
# rv0 == 0
rv1 = external_module.api_call()
# rv1 == 1
assert_called_with
assert_called_with
asserts that the patched function was called with the arguments specified as arguments to assert_called_with
.
[inside some_func]someAPI.API_call(foo, bar='baz')[inside test_some_func]some_func()mock_api_call.assert_called_with(foo, bar='baz')
A full example
In this example, I'm testing a retry
function on Client.update
. This means that the API calls in update
will be made twice, which is a great time to use MagicMock.side_effect
.
The full code of the example is here:
import unittestfrom unittest.mock
import patchclass TestClient(unittest.TestCase):
def setUp(self):
self.vars_client = VarsClient()
@patch('pyvars.vars_client.VarsClient.get')
@patch('requests.post')def test_update_retry_works_eventually(self, mock_post, mock_get):
mock_get.side_effect = [VarsResponse(),VarsResponse()]
mock_post.side_effect = [requests.ConnectionError('Test error'),
MagicMock(status_code=200,
headers={'content-type':"application/json"},
text=json.dumps({'status':True})) ]
response = self.vars_client.update('test', '0')
self.assertEqual(response, response)
@patch('pyvars.vars_client.VarsClient.get')
@patch('requests.post')
def test_update_retry_works_eventually(self, mock_post, mock_get):
I'm patching two calls in the function under test (pyvars.vars_client.VarsClient.update
), one to VarsClient.get
and one to requests.post
. Since I'm patching two calls, I get two arguments to my test function, which I've called mock_post
and mock_get
. These are both MagicMock
objects. In their default state, they don't do much. We need to assign some response behaviors to them.
mock_get.side_effect = [ VarsResponse(), VarsResponse()]
mock_post.side_effect = [ requests.ConnectionError('Test error'),' MagicMock(status_code=200, headers={'content-type':"application/json"}, text=json.dumps({'status':True}))]
This tests to make sure a retry facility works eventually, so I'll be calling update multiple times, and making multiple calls to VarsClient.get
and requests.post
.
Here I set up the side_effect
s that I want. I want all the calls to VarsClient.get
to work (returning an empty VarsResponse
is fine for this test), the first call to requests.post
to fail with an exception, and the second call to requests.post
to work. This kind of fine-grained control over behavior is only possible through mocking.
response = self.vars_client.update('test', '0')self.assertEqual(response, response)
Once I've set up the side_effect
s, the rest of the test is straightforward. The behavior is: the first call to requests.post
fails, so the retry facility wrapping VarsClient.update
should catch the error, and everything should work the second time. This behavior can be further verified by checking the call history of mock_get
and mock_post
.
Conclusion
Using mock objects correctly goes against our intuition to make tests as real and thorough as possible, but doing so gives us the ability to write self-contained tests that run quickly, with no dependencies. It gives us the power to test exception handling and edge cases that would otherwise be impossible to test. Most importantly, it gives us the freedom to focus our test efforts on the functionality of our code, rather than our ability to set up a test environment. By concentrating on testing what’s important, we can improve test coverage and increase the reliability of our code, which is why we test in the first place.
Documentation Links
https://docs.python.org/3/library/unittest.mock.html
Sécurité IaC conçue pour les développeurs
Snyk assure la sécurité de votre infrastructure en tant que code du cycle du développement logiciel à son exécution dans le cloud avec un moteur de politique en tant que code unifié pour permettre à chaque équipe de développer, déployer et faire fonctionner les solutions en toute sécurité dans le cloud.