March 29, 2023, by Raffaele Colace
Tech
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.
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.
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.
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.