Introduction
Unit testing is an essential part of our daily struggles as a programmer. Therefore, if we want to be better in our work we have to advance in this approach.
In the article, we are going to look at the mock module of Python’s unittest library. In the end, you will end up with levelled up unit testing skills.
What is mocking?
This is actually a test technique that allows you to isolate parts of your code and unit test them. You can easily fake function calls in your tests wherever needed.
In simple words, you can manipulate the return value and the side effects (e.g. raising an exception) of your functions however you want.
Yes, I was confused at the beginning too but let’s dive into these inscrutable places with some examples!
Why do I have to use it?
I really hate when someone tells me ‘Learn this, it’s helpful!’. That’s why I will list some cases where you can use mocking technique before starting with the examples:
- Calling functions in other functions
- Working with datetime
- Calling third-party apps
Calling functions
In order to keep with unit test practices, we have to test single parts of our system independently. This means that if we have a function parent_func()
that calls function child_func()
we have to test them separately.
Usually, the result from child_func()
is essential for parent_func()
– the result of parent_func()
depends on the result of child_func()
. This is incongruous with the principals of unit testing. How can I still unit test then?
Simple example
Let’s say we have the following function:
# examples.py
from random import randint
def get_number():
number = randint(1, 100)
if number % 2 == 0:
return 'Even number: {}'.format(number)
return 'Odd number: {}'.format(number)
Here we call the built-in randint()
function from the random
module. We are sure (hopefully) that python core developers have already tested this function so it would be а waste of time and resources to do it again!
Instead, we are going to test our get_number()
function. But how can I test something that is generated randomly every time? The answer is simple – mock it!
# test_examples.py
from unittest import TestCase, mock, main
from examples import get_number
class GetNumberTests(TestCase):
@mock.patch('examples.randint')
def test_get_number_with_even_number(self, randint_mock):
randint_mock.return_value = 42
result = get_number()
self.assertEqual('Even number: 42', result)
@mock.patch('examples.randint')
def test_get_number_with_odd_number(self, randint_mock):
randint_mock.return_value = 69
result = get_number()
self.assertEqual('Odd number: 69', result)
if __name__ == '__main__':
main()
And the output from python test_examples.py
is:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
What does actually happens?
First of all, what does @mock.patch(examples.randint)
mean? You actually tell Python “If you see the randint()
method in the examples module
– mock it’s call!”. That’s how it very works.
Once you mock the call of a function it gives mock object
argument to your test function but you have to pass it. You can name it whenever you want. If you don’t pass it to the function a TypeError
is raised – “test_func() expects 2 arguments but 1 was given”.
OK, so now we have a randint_mock
object and we have to do something with it! What we really want is to manipulate the return value of the randint()
function.
This can be easily done by changing the return_value
of your mock objects. By default it is None
but you can literally set it to any Python object.
If you haven’t done it yet check the official Python documentation for unittest.mocks.
Datetime example
Here is an example that you may find really handy one day.
# in examples.py
from datetime import datetime
def is_summer():
month = datetime.now().month
return month in [6, 7, 8]
Yes, I agree, this might not be the most useful function but the test we are going to write is. This is how it looks like:
class IsSummerTests(TestCase):
@mock.patch('examples.datetime.now') # <--- dafuq?
def test_is_summer_in_august(self, now_mock):
now_mock.return_value = datetime(year=2000, month=8, day=1)
self.assertTrue(is_summer())
Let’s run it:
E
======================================================================
ERROR: test_is_summer_in_august (__main__.IsSummerTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python3.5/unittest/mock.py", line 1149, in patched
arg = patching.__enter__()
File "/usr/lib/python3.5/unittest/mock.py", line 1312, in __enter__
setattr(self.target, self.attribute, new_attr)
TypeError: can't set attributes of built-in/extension type 'datetime.datetime'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/lib/python3.5/unittest/mock.py", line 1170, in patched
patching.__exit__(*exc_info)
File "/usr/lib/python3.5/unittest/mock.py", line 1332, in __exit__
setattr(self.target, self.attribute, self.temp_original)
TypeError: can't set attributes of built-in/extension type 'datetime.datetime'
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (errors=1)
Well, it doesn’t work as expected. Why?
You may notice that I’m trying to mock a function from built-in module. We did this in the previous example and saw it is not that hard. The problem in this example is that we are accessing now()
via the datetime class
of the datetime.module
.
That’s why this mock is impossible in fact. At least, datetime is not even part of our examples module
. So it is impossible to test a function that uses the datetime module? Of course, the answer is no.
The right way of testing datetime
The solution is so simple. We can create our function that actually wraps/returns the datetime.now()
object. Once we have it, we can mock it’s call! This is how our example is going to look like:
# in examples.py
from datetime import datetime
def get_now():
return datetime.now()
def is_summer():
month = get_now().month
return month in [6, 7, 8]
Now we can test is_summer()
:
from unittest import TestCase, mock, main
from datetime import datetime
from examples import is_summer
class IsSummerTests(TestCase):
AUGUST = 8
DECEMBER = 12
@mock.patch('examples.get_now')
def test_is_summer_in_august(self, now_mock):
now_mock.return_value = datetime(year=2000, month=self.AUGUST, day=1)
self.assertTrue(is_summer())
@mock.patch('examples.get_now')
def test_is_summer_in_december(self, now_mock):
now_mock.return_value = datetime(year=2000, month=self.DECEMBER, day=1)
self.assertFalse(is_summer())
if __name__ == '__main__':
main()
Let’s run it and see if it works this time:
....
----------------------------------------------------------------------
Ran 4 tests in 0.002s
OK
Congratulations! You have just mocked the present…
Calling third-party apps
As we know, calling third-party apps in our projects is a pretty common case. Check my previous article for a more detailed analysis – Handle errors from third-party apps in Celery & Django.
That puts us in trouble: – If the third-party server has some issues our tests will fail and we cannot predict that – If we don’t have an Internet connection our tests will fail and we cannot predict that
One of the worst things about programming is when you cannot predict your code! This is the time where our mocking skills come in place.
Simple Example
This is basically how calling a third party looks like:
from third_party import call_me_and_get_payload
def call_third_party():
result = call_me_and_get_payload()
return result
Test & Mock
Here is how our tests will look like:
from unittest import TestCase, mock, main
from examples import call_third_party
class CallThirdPartyTests(TestCase):
@mock.patch('examples.call_me_and_get_payload')
def test_call_third_party_returns_desired_payload(self, call_mock):
call_mock.return_value = {}
call_third_party()
# Put your assertions here:
# It's common to have some database transactions connected with the
# third party payload.
# You can assert them here
if __name__ == '__main__':
main()
That’s how you are not actually calling the third party so you can predict the behavior of your tests and code.
Conclusion
We looked at some common cases where we can use unit test mocking. Even though we have mastered unit testing it is not enough to be sure our system works correctly.
You may also need some integration tests but having a lot unit tests is always a good way to ensure yourself that your code is covered and works the right way!