Aiming at complete code coverage by unit tests tends to be cumbersome, especially for cases where external API calls a part of the code base. For these reasons, Python comes with the unittest.mock library, appearing to be a powerful companion in replacing parts of the system under test.
4. who am i
• database engineer
• currently keeps Oracle Exadata systems up and running
• uses Python to automate routine tasks
5. unit tests
• a piece of software that tests parts of your code base
• isolate a unit and validate its correctness
• form the basic pillar of Test-Driven Development
• should be written before actually implementing the intended
functionality
• commonly automated as part of your CI pipeline
• frameworks for all common languages, e.g. JUnit, NUnit, …
• Python comes with pytest
6. unit tests
test_matlib.py:
from matlib import div
def test_div():
assert div(4,2) == 2
$ pytest -v
collected 0 items / 1 errors
ImportError while importing test module
'/home/debadmin/mock_talk/unit_test/test_matlib.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
test_matlib.py:1: in <module>
from matlib import div
E ModuleNotFoundError: No module named 'matlib'
============================== 1 error in 0.36 seconds ================
7. unit testsmatlib.py:
def div(dividend, divisor):
if divisor != 0:
result = dividend / divisor
else:
print('not defined')
result = float('NaN')
return result
$ pytest -v
collected 1 item
test_div.py::test_div PASSED [100%]
=== 1 passed in 0.01 seconds =====================================
8. code coverage
• determines the percentage of code lines executed by a unit test
• should be close to 100%
• reports should be part of your CI pipeline
• provided by the pytest-cov plugin
• nicely integrates with Jenkins’ Cobertura plugin
15. fake tests
def unit_under_test:
.
SendEmail.send_message(msg)
.
class SendEmail:
.
.
def send_message(self, message):
mail_client = smtplib.SMTP(‘localhost’)
mail_client.send_message(message)
mail_client.close()
.
.
• running this in a test suite would spark off unintended mails
• however, seen from a code coverage perspective it would suffice to know
whether SendEmail.send_message has been called
16. mock
You use mock to describe something which is not real or genuine, but
which is intended to be very similar to the real thing. (Collins
Dictionary)
In object-oriented programming, mock objects are simulated objects
that mimic the behaviour of real objects in controlled ways.
(Wikipedia)
17. use cases
• avoid undesired effects of API calls
• deal with non deterministic results, e.g. current time
• decrease runtime of your test suite
• deal with states that are difficult to reproduce, e.g. network error
• break dependency chain
• reduce complexity of your unit test setup/teardown
18. unittest.mock
unittest.mock is a library for testing in Python. It allows you to replace
parts of your system under test with mock objects and make
assertions about how they have been used.
20. unittest.mock.patch
def get_current_working_path():
path = os.getcwd()
return path
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
pytest takes the patch
decorator into consideration
and passes an instance of
MagicMock() to the test
function
21. unittest.mock.patch
def get_current_working_path():
path = os.getcwd()
return path
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
mocked_getcwd.return_value =
'/home/debadmin/mock_call'
before calling get_current_working_path()
the attribute return_value is set, which is
acutally the fake
22. unittest.mock.patch
def get_current_working_path():
path = os.getcwd()
return path
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
mocked_getcwd.return_value =
'/home/debadmin/mock_call'
path = get_current_working_path()
since everything is arranged
time to call the unit under test
23. unittest.mock.patch
def get_current_working_path():
path = MagicMock(
return_value =
'/home/deba..
)
return path
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
mocked_getcwd.return_value =
'/home/debadmin/mock_call'
path = get_current_working_path()
os.getcwd() is replaced by a
MagicMock object
24. unittest.mock.patch
def get_current_working_path():
path = MagicMock(
return_value =
'/home/deba..
)
return path
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
mocked_getcwd.return_value =
'/home/debadmin/mock_call'
path = get_current_working_path()
the faked path will be
returned to the caller
26. unittest.mock
import os
def get_current_working_path():
path = os.getcwd()
print ('ncurrent directory is : ' + str(path))
return path
if __name__ == '__main__':
current_working_path = get_current_working_path()
debadmin@jenkins1:/tmp/mock_talk>python sample1.py
current directory is : /tmp/mock_talk
debadmin@jenkins1:/tmp/mock_talk>
27. unittest.mock.patch
>>> from unittest import mock
>>> with mock.patch('os.getcwd') as mocked_getcwd:
... dir(mocked_getcwd)
...
mocking out os.getcwd by
means of an context
manager
possible assertions about
mock object usage
['assert_any_call', 'assert_called', 'assert_called_once',
'assert_called_once_with', 'assert_called_with', 'assert_has_calls',
'assert_not_called', 'attach_mock', 'call_args', 'call_args_list',
'call_count', 'called', 'configure_mock', 'method_calls',
'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value',
'side_effect']
>>>
28. unittest.mock.patch
debadmin@jenkins1:~/mock_talk$ pytest -v -s -k test_call_only
=========== test session starts =======================================
platform linux -- Python 3.7.2, pytest-4.3.0, py-1.8.0, pluggy-0.9.0 --
/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /home/debadmin/mock_talk, inifile:
collected 1 item
test/test_sample1.py::test_call_only
current directory is : <MagicMock name='getcwd()' id='139723199533968'>
PASSED
========== 1 passed in 0.01 seconds =====================================
os.getcwd has been
replaced by a MagicMock
object
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
path = get_current_working_path()
mocked_getcwd.assert_called_once()
29. unittest.mock.patch
debadmin@jenkins1:~/mock_talk$ pytest -v -s -k test_return_value
=========== test session starts =======================================
platform linux -- Python 3.7.2, pytest-4.3.0, py-1.8.0, pluggy-0.9.0 --
/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /home/debadmin/mock_talk, inifile:
collected 1 item
test/test_sample1.py::test_return_value
current directory is : /home/debadmin/mock_call
PASSED
========== 1 passed in 0.01 seconds =====================================
@mock.patch('os.getcwd')
def test_call_only(mocked_getcwd):
mocked_getcwd.return_value = '/home/debadmin/mock_call'
path = get_current_working_path()
assert path == '/home/debadmin/mock_call'
faked return value
print considers faked
return value
30. unittest.mock.patch
@mock.patch('subprocess.check_call')
def test_stop_database(self, mock_check_call):
fb = Flashback(
restore_point_name='TEST1',
oracle_home='/opt/oracle/product/12.1.0.2.180417/db_test',
oracle_sid='TTEST1'
)
fb._stop_database()
mock_check_call.assert_called_with(
['/opt/oracle/product/12.1.0.2.180417/db_test/bin/srvctl',
'stop',
'database',
'-d',
'TTEST_XD0104',
]
def _stop_database(self):
LOG.info('stopping database %s ...', self.db_unique_name)
subprocess.check_call(
[self.srvctl_command, 'stop', 'database', '-d', self.db_unique_name]
)
srvctl deals with stopping
a cluster database, which
is not available for the
test system
testing the srvctl command is
out of scope, however,
determining that is has been
called with appropriate
parameters is of interest
31. unittest.mock.patch
@mock.patch('rlb.common.oradb.DatabaseLib.sql_plus_task')
def test_run_flashback_restore(self, mock_sql_plus_task):
fb = Flashback(
restore_point_name='TEST1',
oracle_home='/opt/oracle/product/12.1.0.2.180417/db_test',
oracle_sid='TTEST1'
)
fb._run_flashback_restore()
mock_sql_plus_task.assert_any_call(
'nflashback database to restore point TEST1;n'
)
mock_sql_plus_task.assert_any_call(
'alter database open resetlogs;'
)
def _run_flashback_restore(self):
LOG.info('restoring database ... ')
self.dl.sql_plus_task(< restore command >)
self.dl.sql_plus_task('alter database open resetlogs;')
2 calls of sql_plus_task
with different parameters
the restore command
the open database
command
32. references
• Kent B. (1999). Extreme Programming. Addison-Wesley.
• unittest.mock – mock object library. [online] Available at:
https://docs.python.org/3/library/unittest.mock.html
• What the mock? – A cheatsheet for mocking in Pyhton. [online] Available at:
https://medium.com/@yeraydiazdiaz/what-the-mock-cheatsheet-mocking-in-
python-6a71db997832
34. speaker unit test
If you liked the talk leave a comment at https://www.pydays.at/feedback
If you didn‘t like the talk leave a comment but be gentle
Thx for your attention and enjoy your lunch