Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Jamal Moir: Mocking in Python - How to Bypass Expensive and External Code Within Your Tests

$
0
0


Do you want to be able to utilise the powerful tool that is mocking within your Python tests?

Mocking is a useful tool and was vital to unit tests that I needed to write for a project I am working on. I'd never used mocking before, so I didn't know where to start but after reading pages of documentation and example code, I've got it. In this post I'll tell you what I learnt from sifting through all that information, so you don't have to.

In this post, we will be covering:

  • What is Mocking?
  • Mocking in Python
  • Using Patch
  • Using Patch.object
  • Customising Your Mocks' Responses
  • Providing a Spec for Your Mocks
  • Using Mock's Assertions
  • An Example From a Project of Mine

WHAT IS MOCKING?

Rather than explaining what it is straight out, I think it's first better to understand why we need mocking.

Let's say you have a web or desktop application that needs to get foreign exchange rates via a web API hosted by some external source. This API is an external dependency, your code needs it to function but you have no control over it.

When unit testing your code, sending and receiving HTTP requests and responses constantly will take time, time that you don't really want to waste and that will become tiresome as your project progresses. What if you could bypass this HTTP requesting and receiving and just test how your code handles it; you don't have control over the web API anyway, so there's no point testing it. Well, that my friends is where mocking comes in.

Mocking replaces external dependencies with controllable objects that simulate the behaviour of that foreign code. This allows you to focus on testing your code, and not that of the web API or whatever the external dependency is. It also has the benefit of cutting out the time that it takes to connect and interact with that outside source.

Mocking can also be used to avoid executing expensive code, in other words code that takes a lot of time or resources to run its course. This allows your tests to be snappy and for you to concentrate on the development of your application rather than waiting for tests to execute.

MOCKING IN PYTHON

Although other mocking libraries are available, we will be focusing on Mock, which as of Python 3.3 is included within the standard library (unittest.mock). If you are a straggler, too stubborn to make the move to Python 3 (just kidding(?)), then you can get a backported version of the standard library's unittest.mock here.

The workflow that you use when working with unittest.mock is simple and a lot like your normal workflow, but with a couple of extra steps. 

This is the general workflow when using unnitest.mock:
  1. Look for the external dependencies that need to be mocked out
  2. Patch the things that you found in step one
  3. Customise the responses that you want your mocks to give
  4. Write your tests
These steps can be shuffled around a bit, for example you could write the tests as you normally would and then follow steps 1-3. It depends on personal preference. I personally feel that writing the tests first and then editing them to work with mocks is a waste of my time when I can just write the mocks into the test from the beginning.

That's it. Simple, right?

USING PATCH

You patch classes, methods and functions via unnittest.mock.patch in order to hijack expensive functions with MagicMock objects. These objects then allow you to test your program without executing code that takes a lot of time or resources to execute for example. You can use patch either as a decorator or as a context manager.

Using decorators:
@patch('external.web.Api')
@patch('webbrowser.open')
def test_foo(mock_wbopen, MockApi):
# Test code.

A (not so) fun fact about this to remember is that in Python, if you have more than one decorator decorating a function, they are processed from the closest one to the function to the furthest one from the function. In other words, from bottom to top. The function's parameters have to reflect this and so in effect, have to be entered in reverse order. This in my opinion is not good; it's counter intuitive and is easy to trip up on. Make sure you remember this while writing your tests.

Using context managers:
def test_foo():                                                         
with patch('external.web.Api') as MockApi, \
patch('webbrowser.open') as mock_wbopen:
# Test code.

The syntax for this is as always with a context manager.

USING PATCH.OBJECT

The patch.object function is used to patch an object's member with a mock object.

@patch.object('external.web.Api', 'get_response')
def test_foo(get_response):
# Test code.

Like in the example, patch.object can be used as a decorator, but it can also be used with a context manager as well. The first argument is the class and the second is the member of the object to be mocked.

USING PATCH.DICT

You can use patch.dict when you need to patch a dictionary, or 'dictionary-like' object. Once the test has been completed the dictionary will be reverted to it's original state.

names = {}
with patch.dict(names, {'f_name': 'Jamal', 'l_name': 'Moir'}):
# Test code

As shown in the above example, you use patch.dict as a context manager, passing it the dictionary to test with. You can then test it in however way you need and it will be reverted back to how it was once the test is over.

CUSTOMISING YOUR MOCKS' RESPONSES

Once you have patched everything that needs to be patched, you will probably want to customise these MagicMock objects. You can customise the values that they store, the responses of their methods and functions, and even make them throw exceptions. By utilising this customisation, you can create every kind of response you can think of and extensively test your code.

Customising responses from methods and functions:


# Set external API's return value to False
web_api = MockApi.return_value
response = web_api.get_response.return_value
response = False

The above example first creates a variable which represents a MockApi object, this is what MockApi() would return. It then creates another variable which represents the value that is returned by the get_response() method and sets it to False. Now, when the test is ran the MockApi object's get_response() method will return false.

Making your MagicMock objects raise exceptions:


  # Set webbrowser.open to raise an exception
mock_wbopen.side_effect = webbrowser.Error('Something bad happened.')

This example sets a Mock object's property called side_effect to an exception. The side_effect property can be set to a function, iterable or an exception. When the mock is called, if it's given a function, the function will be called, if it's given a iterable, each time the mock is called the iterable will yield a value and if it's given an exception the exception will be raised.

PROVIDING A SPEC FOR YOUR MOCKS

You get given a lot of freedom with Mock objects. By default they will behave like they have any field, method or function you try to fetch, which can lead to problems. Sometimes you'll want to limit what you can do and that is what specs are for. Specs can be passed as an argument when defining your mocks or can be added later via mock_add_spec().

By adding a spec to your mock, you will only be able to fetch what the class you are using as a spec has. If you try to fetch something that is not a part of that class it will raise an AttributeError. By providing a spec_set when defining your mock instead of a spec, or by setting spec_set=True in mock_add_spec() you can also disallow the setting of attributes not defined in the set as well as the fetching of them.

# Using 'spec' argument
mock_foo = MockFoo(spec=ExampleClass)

# Using mock_add_spec.
mock_foo = MockFoo()
mock_foo.mock_add_spec(spec=ExampleClass, spec_set=True)

Alternatively, when patching using patch() and the like, you can pass spec=True or spec_set=True, which will pass the object that is being patched as the spec or spec_set.

# Adding a spec when patching.
@patch('external.web.Api', spec=True)

USING MOCK'S ASSERTIONS

assert_called_with
mock_foo = MockFoo()
mock_foo.bar('this', 'this', 'and this', also='this')
mock_foo.bar.assert_called_with('this', 'this', 'and this', also='this') # This would return true.
You can use assert_called_with() to check to see if your calls are made correctly. This is a good way to check if objects have been initialised correctly, or connections have been made with the right authorisation details, etc. It will only return true if the last call was made with the supplied arguments.

assert_called_once_with
mock_foo = MockFoo()
mock_foo.bar('this', 'this too', and='this')
mock_foo.bar.assert_called_once_with('this', 'this too', and='this') # This would return true.
Use assert_called_once_with() when you want to make sure that something was called only once. If it is called zero or more than one times this method will return false.

assert_not_called
mock_foo = MockFoo()
test = mock_foo.test()
mock_foo.test.assert_not_called() # This would return false.
This method, assert_not_called() checks whether a mock has been called. If it hasn't been called it will return true.

assert_any_call
mock_foo = MockFoo()
mock_foo('this', 'this', and='this')
mock_foo('this', 'this', 'this too')
mock_foo.assert_any_call('this', 'this', and='this') # This would return true.
The method assert_any_call works similarly with assert_called_with in that if the mock was called with the specified arguments it will return true. The difference is that assert_called_with will only return true if it was the last call made to to mock, whereas assert_any_call will return true if the mock was called with the specified arguments at some point.

assert_has_calls
mock_foo = MockFoo()
mock_foo('this')
mock_foo('this')
mock_foo('and this')
mock_foo('this too')
calls = [call('this'), call('and this')]
mock_foo.assert_has_calls(calls) # This would return true.
calls = [call('this'), call('this too'), call('and this')]
mock_foo.assert_has_calls(calls) # This would return false.
mock_foo.assert_has_calls(calls, any_order=True) # This would return true.
If you want to check if multiple different calls have been made to your mock object, then assert_has_calls is what you want. You pass it a list of arguments that you want your mock to be called with. 

It also has an optional parameter which allows you to specify whether the order that the calls were made matters. For example if you specify the calls [1, 2, 3] and set any_order=True then if the mock was called with the values 3, 2 and 1 in that order, it will return true. If any_order was set to false however, this would return false.

AN EXAMPLE FROM A PROJECT OF MINE

Previously I mentioned that I had to learn how to use mocking for a project I'm working on, this example code is from that. It's a desktop blogging client written in python aimed to fill the gap of such software on linux.

This example is testing the process of authentication with Google's API. If I didn't mock this, every time the tests were ran, they would open the default browser, mess around with local storage and ask for input. This would take a lot of time and break the automation of the unit tests, mocking lets us bypass these.

@patch('oauth2client.client.flow_from_clientsecrets')                           
@patch('oauth2client.file.Storage')
@patch('webbrowser.open')
@patch('pyblogit.api_interface.input')
def test_get_credentials(mock_input, mock_wbopen, MockStorage, mock_flow):
# Set credentials to invalid.
storage = MockStorage.return_value
credentials = storage.get.return_value
credentials.invalid = True

# Run the method and see if we get what we want.
result = pyblogit.api_interface.BloggerInterface().get_credentials()

# Check that the flow was initialised correctly.
mock_flow.assert_called_with('client_secret.json',
'https://www.googleapis.com/auth/blogger',
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
MockStorage.assert_called_with('credentials.dat')

# With invalid credentials, the code should obtain an auth
# url from the flow and pass it to the browser. Then the
# authentication code should be taken from input and passed
# back to the flow for exchange. Test these interactions
# took place.
flow = mock_flow.return_value

flow.step1_get_authorize_url.assert_called_once_with()
mock_wbopen.assert_called_once_with(
flow.step1_get_authorize_url.return_value)
flow.step2_exchange.assert_called_once_with(mock_input.return_value)
storage.put(flow.step2_exchange.return_value)

assert result == flow.step2_exchange.return_value


Once you get your head around it, mocking is actually quite simple to do and it's such a powerful tool to level up your testing suite. Mocking is something that since I have learnt how to utilise, I really don't know how I wrote effective tests without it... I probably didn't.

Well, that's it for this post on mocking, I hope you've managed to learn something useful. Make sure you share, tweet, pin or whatever the cool kids are doing nowadays so that other people can read this too. Also, follow me on twitter and subscribe to my posts feed so you don't miss any future posts.

Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>