Before we start writing tests let’s make sure that we understand why do we want to write unit tests and the concept of unit testing. Here are a few reasons (from my experience) why is a good idea to write tests:
- In more complex projects you can’t (or it’s very hard) simulate the error that you have found in your error.log. So if you write unit test that will simulate that error you can then fix your code and when test pass you know you have fix the bug.
- It is very hard to check if some functionality is working when it gets its input from some other service that isn’t build yet (or you only know how it behaves). You can write unit test that will simulate (mock) that service.
- On the long run every test you write will save you time. FACT!
For conceptual part I will use this quote:
As a developer, you care more that your library successfully called the system function for ejecting a CD as opposed to experiencing your CD tray open every time a test is run.
Ok lets look at some examples. First we will start with some really simple ones (that I have “stolen” from toptal):
This is our code that we want to test:
# -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)py
With mock lib we can easily test our code like this:
# -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with("any path")
Note: we test only if os.remove is successfully called with correct arguments.
Let see how we can test Pyramid/Django view function:
@view_config( route_name='generate_deeplink', renderer='myProject:templates/generate_deeplink.mako', permission="view", ) def generate_deeplink(request): if request.POST: try: booking = ExternalAPI( request.POST["reserv_number"].strip(), request.POST["email"].strip(), ).get() return dict(booking=booking) except AdministrationException as e: request.session.flash( 'AdministrationException: {}'.format(e)) return dict()
And the test for this piece of code can be something like this:
BOOKING_INFO_RS = """ <RS> <Administration> <Errors/> </Administration> <Responses> <ReservationRS status="PENDING" test="tist"> <ReservNo>123456789</ReservNo> </ReservationRS> </Responses> </RS> """ @mock.patch('myProject.views.ExternalAPI') def test_generate_deeplink(self, external_API_rq): req = testing.DummyRequest(post={ 'reserv_number': '12345678 ', 'email': 'john.doe@example.com', }) external_API_rq().get.return_value = BOOKING_INFO_RS result = views.generate_deeplink(req) external_API_rq.assert_called_with( '12345678', 'john.doe@example.com', ) self.assertEqual(result['booking'], BOOKING_INFO_RS)
So before we call views.generate_deeplink(req)
we create dummy request and mock ExternalAPI and we set ExternalAPI.get() function return value. After we call our view we start checking if ExternalAPI was called with the correct params and if the result is what we expect.
Here is a bit more complex example that is also using very useful freezegun lib.
@view_config(route_name='send_deeplink', permission="view") def send_deeplink(request): _ = request.localizer.translate reserv_number = request.matchdict['reserv_number'] email = request.matchdict['email'] booking = ExternalAPI(reserv_number, email).get() json_querystring = ExternalAPI_checking_something( email, reserv_number, lang=langs.get(request.localizer.locale_name), data1=booking["data1"], data2=booking["data2"], data3=booking["data3"], ).query() deeplink = request.route_url( "some_route", lang=request.localizer.locale_name, query=json_querystring, booking_number=reserv_number, email=email, ) email_template = Template( filename=request.registry.get("BASE") + '/myProject/customer/templates/mail_deeplink.mako', input_encoding='utf-8', output_encoding='utf-8', ) body = email_template.render( request=request, deeplink=deeplink, booking=booking, date=datetime.now().strftime("%d.%m.%Y"), _=_, ).decode('utf_8') author = request.registry.get("mail_default_sender") subject = _("SUBJECT") message = Message( subject=subject, sender=author, recipients=[email], html=body, ) request.registry['mailer'].send_immediately(message, fail_silently=False) request.session.flash('Deeplink successfully sent.') return HTTPFound(location=request.route_url('generate_deeplink'))
And our unit test:
# -*- coding: utf-8 -*- from freezegun import freeze_time from pyramid import testing from pyramid.httpexceptions import HTTPFound from myProject import views import mock import os import unittest BOOKING_INFO_DICT = { 'data1': { 'id': '15', 'time': '12:00', }, 'data2': { 'id': '12', 'time': '12:00', }, 'data3': '29', } QUERY_STRING = 'param_1%3Dvalue_1%2Bparam_2%26param_3%3Dvalue_3%26' BASE_PATH = os.path.abspath(os.path.join(os.pardir, os.pardir)) DEEPLINK = 'this is mocked deeplink' @freeze_time("2015-01-14 12:00:01") class TestDeeplink(unittest.TestCase): def setUp(self): self.config = testing.setUp() def tearDown(self): testing.tearDown() @mock.patch('myProject.views.Message') @mock.patch('myProject.views.Template') @mock.patch('myProject.views.ExternalAPI_checking_something') @mock.patch('myProject.views.ExternalAPI') def test_generate_deeplink( self, external_API_rq, external_API_2_rq, check_booking, mock_Template, mock_Message, ): req = testing.DummyRequest() req.localizer.locale_name = 'en' req.registry = { 'BASE': BASE_PATH, 'mail_default_sender': 'admin@localhost.com', 'mailer': mock.MagicMock() } req.registry['mailer'].send_immediately = mock.MagicMock() req.matchdict = { 'email': 'john.doe@example.com', 'reserv_number': '12345678', } external_API_2_rq().query.return_value = QUERY_STRING external_API_rq().get.return_value = BOOKING_INFO_DICT req.route_url = mock.MagicMock() req.route_url.return_value = DEEPLINK mock_Template().render.return_value = 'rendered html email' req.session.flash = mock.MagicMock() mock_Message.return_value = 'awesome email' result = views.send_deeplink(req) external_API_rq.assert_called_with( '12345678', 'john.doe@example.com', ) # because `request.route_url` is called 2 times we must check if it # was called in the right order and with the correct params req.route_url.assert_has_calls([ mock.call( 'some_route', lang='en', query=QUERY_STRING, booking_number='12345678', email='john.doe@example.com', ), mock.call('generate_deeplink'), ]) # checking if `Template` was called with the correct params mock_Template.assert_called_with( filename=os.path.join( BASE_PATH, 'myProject/templates/mail_deeplink_bookRQ.mako', ), input_encoding='utf-8', output_encoding='utf-8', ) # checking if `Template.render()` was called with the correct params mock_Template().render.assert_called_with( request=req, deeplink=DEEPLINK, booking=BOOKING_INFO_DICT, date='14.01.2015', # time that we have set with `@freeze_time` _=req.localizer.translate, ) # ... mock_Message.assert_called_with( subject='SUBJECT', sender='admin@localhost.com', recipients=['john.doe@example.com'], html='rendered html email', ) # ... req.registry['mailer'].send_immediately.assert_called_with( 'awesome email', fail_silently=False, ) # ... req.session.flash.assert_called_with( 'Deeplink successfully sent.') # checking if the return result is redirect - HTTPFound instance self.assertIsInstance(result, HTTPFound)
First we mock variables and functions and then we set return values for these functions (e.g. mock_Template().render.return_value = 'rendered html email'
). After the stage is set we call our view ( result = views.send_deeplink(req)
) and then we start checking what was called ( e.g. ...assert_called_with(...)
).
One thing to remember is to always set your mock functions and return values BEFORE you call your view and check what was called AFTER you call your view ( result = views.send_deeplink(req)
).