Unit tests

Unit tests are the simplest tests to add to a project, and the standard library comes with everything needed to write some. In a project based on Flask, there usually are, alongside the views, some functions and classes, which can be unit-tested in isolation.

However, the concept of separation is quite vague for a Python project, because we don't use contracts or interfaces like in other languages, where the implementation of the class is separated from its definition.

Testing in isolation in Python usually means that you instantiate a class or call a function with specific arguments, and verify that you get the expected result. When the class or function calls another piece of code that's not built in Python or its standard library, it's not in isolation anymore.

In some cases, it will be useful to mock those calls to achieve isolation. Mocking means replacing a piece of code with a mock version, which takes specified input, yields specified outputs, and fakes the behavior in between. But mocking is often a dangerous exercise, because it's easy to implement a different behavior in your mocks and end up with some code that works with your mocks but not the real thing. That problem often occurs when you update your project's dependencies, and your mocks are not updated to reflect the new behaviors, which might have been introduced in some library.

So, limiting the usage of mocks to the three following use cases is good practice:

  • I/O operations: When the code performs calls to third-party services or a resource (socket, files, and so on), and you can't run them from within your tests
  • CPU intensive operations: When the call computes something that would make the test suite too slow
  • Specific behaviors to reproduce: When you want to write a test to try out your code under specific behaviors (for example, a network error or changing the date or time by mocking the date time and time modules)

Consider the following class, which can be used to query a bug list via the Bugzilla REST API, using the requests (http://docs.python-requests.org) library:

    import requests 

class MyBugzilla:
def __init__(self, account, server =
'https://bugzilla.mozilla.org'):
self.account = account
self.server = server
self.session = requests.Session()

def bug_link(self, bug_id):
return '%s/show_bug.cgi?id=%s' % (self.server, bug_id)

def get_new_bugs(self):
call = self.server + '/rest/bug'
params = {'assigned_to': self.account,
'status': 'NEW',
'limit': 10}
try:
res = self.session.get(call, params=params).json()
except requests.exceptions.ConnectionError:
# oh well
res = {'bugs': []}

def _add_link(bug):
bug['link'] = self.bug_link(bug)
return bug

for bug in res['bugs']:
yield _add_link(bug)

This class has a bug_link() method, which we can test in isolation, and one get_new_bugs() method, which performs calls to the Bugzilla server. It would be too complicated to run our Bugzilla server when the test is executed, so we can mock the calls and provide our JSON values for the class to work in isolation.

This technique is used in the following example with request_mock (http://requests-mock.readthedocs.io), which is a handy library to mock request network calls:

    import unittest 
from unittest import mock
import requests
from requests.exceptions import ConnectionError
import requests_mock
from bugzilla import MyBugzilla


class TestBugzilla(unittest.TestCase):

def test_bug_id(self):
zilla = MyBugzilla('[email protected]', server =
='http://example.com')
link = zilla.bug_link(23)
self.assertEqual(link, 'http://example.com/show_bug.cgi?id=23')

@requests_mock.mock()
def test_get_new_bugs(self, mocker):
# mocking the requests call and send back two bugs
bugs = [{'id': 1184528}, {'id': 1184524}]
mocker.get(requests_mock.ANY, json={'bugs': bugs})

zilla = MyBugzilla('[email protected]',
server ='http://example.com')
bugs = list(zilla.get_new_bugs())
self.assertEqual(bugs[0]['link'],
'http://example.com/show_bug.cgi?id=1184528')

@mock.patch.object(requests.Session, 'get',
side_effect=ConnectionError('No network'))
def test_network_error(self, mocked):
# faking a connection error in request if the web is down
zilla = MyBugzilla('[email protected]',
server='http://example.com')

bugs = list(zilla.get_new_bugs())
self.assertEqual(len(bugs), 0)

if __name__ == '__main__':
unittest.main()
You should keep an eye on all your mocks as the project grows, and make sure they are not the only kind of tests that cover a particular feature. For instance, if the Bugzilla project comes up with a new structure for its REST API, and the server your project uses is updated, your tests will happily pass with your broken code until the mocks reflect the new behavior.

The test_network_error() method is a second test which fakes a network error by triggering requests' connection error, using Python's mock patch decorator. This test ensures the class behaves as expected when there's no network.

This kind of unit test is usually enough to cover most of your classes' and functions' behaviors.

This test class will probably cover more cases as the project grows and new situations occur. For instance, what happens if the server sends back a malformed JSON body?

But there's no need to have tests for all the failures you can come up with on day one. In a microservice project, unit tests are not a priority, and aiming at 100% test coverage (where every line of your code is called somewhere in your tests) in your unit tests will add a lot of maintenance work for little benefits.

It's better to focus on building a robust set of functional tests.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset