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)
).