How to write tests in Python and Django using mocking

March 29, 2023, by Raffaele Colace

Tech

Tech

Blog20tab-Techtest-python-mocking

At 20tab we develop software tested with a very high code coverage ratio: during the development phase it never goes below 95% and at the end of the project the expected coverage is always 100%.

When mocking in a test, we are in a certain way making fun of our software or of the function we are testing, simulating the behaviour of a specific external functionality.

In Python there is a package in the standard library that helps us apply mocks during our tests.

But when and why should we mock?

When we write software, we will often need to make calls to functions that are external to our code: for example calls to the operating system, HTTP requests to external APIs, calls to functions we don’t control directly but which our software depends on.

Well, for each of these cases (and not only) it is necessary to simulate the output of external functions in order to carry out consistent software tests.

In this article, I don't want to go into detail on the use of the "unittest.mock" standard library. For that, please refer to the official documentation!

I would rather take this opportunity to write down some useful experiences which could help you save time when writing tests. After all, 20tab is specialized in Python, a language we've been using for more than 10 years, and adopts a TDD approach, which avoids customers' sense of frustration, typical of those who are delivered software full of bugs, and offers them a high quality product.

Mocking on immutable objects

I often encounter problems when performing functionality tests that use the datetime.date class and I need to specifically test a functionality that depends on a certain date.

The first approach consists in mocking the today function of the datetime.date class, as shown below:

exampleapp/functions.py

1 2 3 4 5 6 7 8 from datetime import date def myfunc_using_date(): print("do something") day = date.today() print("do something else") return day

exampleapp/tests.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from datetime import date from unittest.mock import patch from django.test import TestCase from exampleapp.functions import myfunc_using_date def mocked_today(): return date(year=2020, month=1, day=1) class TestImmutableObj(TestCase): @patch("exampleapp.functions.date.today", mocked_today) def test_myfunc_using_date(self): self.assertEqual( myfunc_using_date().strftime("%Y-%m-%d"), "2020-01-01" )

Too bad, however, that we will encounter an execution error when running this test:

python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). E ====================================================================== ERROR: test_myfunc_using_date (exampleapp.tests.TestImmutableObject) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/user/.pyenv/versions/3.7.5/lib/python3.7/unittest/mock.py", line 1247, in patched arg = patching.__enter__() File "/Users/user/.pyenv/versions/3.7.5/lib/python3.7/unittest/mock.py", line 1410, in __enter__ setattr(self.target, self.attribute, new_attr) TypeError: can't set attributes of built-in/extension type 'datetime.date'

There are several ways to work around this issue: the most convenient one was writing a custom function returning the value of today and using it in the code to be able to easily mock it. Let me show you below:

exampleapp/functions.py

1 2 3 4 5 6 7 8 9 10 11 12 from datetime import date def get_today(): return date.today() def myfunc_using_date(): print("do something") day = get_today() print("do something else") return day

exampleapp/tests.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from datetime import date from unittest.mock import patch from django.test import TestCase from exampleapp.functions import myfunc_using_date def mocked_today(): return date(year=2020, month=1, day=1) class TestImmutableObject(TestCase): @patch("exampleapp.functions.get_today", mocked_today) def test_myfunc_using_date(self): self.assertEqual(myfunc_using_date().strftime("%Y-%m-%d"), "2020-01-01")

In this case, the test is correctly executed, meaning that if we carry out consistent checks on our features, we will no longer encounter obstacles when writing our code.

python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). do something do something else . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'...

The code I have presented serves as an example. Here you can find a good library for mocking when using datetime. 

🟣 With our experience and reliability, you can focus on your business goals without worrying about technical aspects.

Discover More

Mocking of http requests

Mocking application is also required in the case of features calling external service APIs. Our software depends on the response we receive, over which, however, we have no control.

The thing that matters the most to us, though, is that our source code is well tested, regardless of the outside world.

Needless to say, in this context, we are always talking about unit tests. If we wanted to be sure of the proper functioning of our software, we would have to write integration tests, but this is the subject of another very broad topic.

Let's see how to solve the problem of depending on external APIs during unit tests.

Let’s pretend we have an endpoint returning, among the many values, also the ones ​​that change with each call. An example? The exact time of the request.

The time will clearly change with each call and therefore, even when we run the test, the result won’t match our expectations, as you can see from the following code:

exampleapp/functions.py

1 2 3 4 5 6 7 8 import requests def call_external_api(): response = requests.get("http://worldtimeapi.org/api/timezone/Europe/Rome") data = response.json() currenttime = data.get("datetime") return currenttime

exampleapp/tests.py

1 2 3 4 5 6 7 8 9 10 11 from django.test import TestCase from exampleapp.functions import call_external_api class TestExternalAPI(TestCase): def test_call_external_api(self): self.assertEqual( call_external_api(), "2020-04-30T12:52:25.020721+02:00" )

The result will obviously be a test failure:

python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================================== FAIL: test_call_external_api (exampleapp.tests.TestExternalAPI) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/user/www/howtomock/exampleapp/tests.py", line 12, in test_call_external_api "2020-04-30T12:52:25.020721+02:00" AssertionError: '2020-04-30T12:52:58.341965+02:00' != '2020-04-30T12:52:25.020721+02:00' - 2020-04-30T12:52:58.341965+02:00 ? - ^^ --- + 2020-04-30T12:52:25.020721+02:00 ? + ^^^^^ ---------------------------------------------------------------------- Ran 1 test in 7.131s FAILED (failures=1) Destroying test database for alias 'default'...

One handy method I have used to solve this issue is to mock the Response class, returning a real json that we will have promptly written in the test and that will be served at the time of the HTTP call.

exampleapp/tests.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from unittest.mock import patch from django.test import TestCase from exampleapp.functions import call_external_api class MockResponse: def __init__(self): self.status_code = 200 def json(self): return { "week_number": 18, "utc_offset": "+02:00", "utc_datetime": "2020-04-30T10:48:54.398875+00:00", "unixtime": 1588243734, "timezone": "Europe/Rome", "raw_offset": 3600, "dst_until": "2020-10-25T01:00:00+00:00", "dst_offset": 3600, "dst_from": "2020-03-29T01:00:00+00:00", "dst": True, "day_of_year": 121, "day_of_week": 4, "datetime": "2020-04-30T12:48:54.398875+02:00", "client_ip": "91.252.18.0", "abbreviation": "CEST" } class TestExternalAPI(TestCase): @patch("requests.get", return_value=MockResponse()) def test_call_external_api(self, mocked): self.assertEqual( call_external_api(), "2020-04-30T12:48:54.398875+02:00" )

This way we will never have to worry about the outside world again and we will be able to focus on testing our features.

Again, the example has a simple didactic purpose.

3 resources for you

  • Click here for a good library that can be used for mocking HTTP requests!
  • The basic sample project I used in this article is located at this link.
  • You can learn more about the 20tab approach to software development here.

🟣 Also check out our Guide to Interactive Mapping with Python and GeoDjango

Learn more