Skip to main content

Python Mocking 101: Fake it before you make it

著者:
wordpress-sync/blog-hero-python-code-purple

2018年2月10日

0 分で読めます

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:

  1. Write the test as if you were using real external APIs.

  2. In the function under test, determine which API calls need to be mocked out; this should be a small number.

  3. In the test function, patch the API calls.

  4. Set up the MagicMock object responses.

  5. 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, MagicMocks 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_effects 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_effects, 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

カテゴリー:
wordpress-sync/blog-hero-python-code-purple

CI/CDパイプラインをレベルアップする方法

プロダクションにプッシュする前に、これらの8つのヒントでパイプ内のセキュリティ問題をキャッチする方法を確認してみましょう⭐️