Unit testing in Python with mock

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

 

Leave a Reply

Your email address will not be published. Required fields are marked *