How to fake properly
by
rainer schuettengruber
who am i
• database engineer
• currently keeps Oracle Exadata systems up and running
• uses Python to automate routine tasks
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
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 ================
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 =====================================
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
code coverage
$ pytest --cov -v
collected 1 item
test_matlib.py::test_div PASSED [100%]
----------- coverage: platform linux, python 3.7.2-final-0 -----------
Name Stmts Miss Cover
------------------------------------
matlib.py 6 2 67%
test_matlib.py 4 0 100%
------------------------------------
TOTAL 10 2 80%
========== 1 passed in 0.03 seconds =====
code coverage
def test_div():
assert div(4,2) == 2
def div(dividend, divisor):
1 if divisor != 0:
2 result = dividend / divisor
3 else:
4 print('not defined')
5 result = float('NaN')
6 return result
this code path is not
considered, therefore code
coverage of 67% (4/6)
code coverage
testmatlib.py:
import math
from matlib import div
def test_div():
assert div(4,2) == 2
def test_div_by_zero():
assert math.isnan(div(4,0))
$ pytest --cov -v
collected 2 items
test_matlib.py::test_div PASSED [ 50%]
test_matlib.py::test_div_by_zero PASSED [100%]
----------- coverage: platform linux, python 3.6.6-final-0 -----------
Name Stmts Miss Cover
------------------------------------
matlib.py 6 0 100%
test_matlib.py 6 0 100%
------------------------------------
TOTAL 12 0 100%
branch coverage
def my_partial_fn(x):
if x:
y = 10
return y
def test_my_partial_fn():
assert my_partial_fn(1) == 10
----------- coverage: platform linux, python 3.6.6-final-0 -----------
Name Stmts Miss Cover
-----------------------------------
mylib.py 4 0 100%
test_mylib.py 3 0 100%
-----------------------------------
TOTAL 7 0 100%
code coverage 100%, however, code is bound to fail …
>>> my_partial_fn(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in my_partial_fn
UnboundLocalError: local variable 'y' referenced before assignment
code coverage
there is room for
improvement
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
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)
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
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.
unittest.mock.patch
def get_current_working_path():
path = os.getcwd()
return path
needs to be
replaced within
the scope of the
unit test
@mock.patch('os.getcwd')
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
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
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
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
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
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()
assert path == '/home/debadmin/mock_call'
test condition evaluates to
true
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>
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']
>>>
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()
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
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
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
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
questions and answers
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

How to fake_properly

  • 1.
    How to fakeproperly by rainer schuettengruber
  • 4.
    who am i •database engineer • currently keeps Oracle Exadata systems up and running • uses Python to automate routine tasks
  • 5.
    unit tests • apiece 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 matlibimport 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 • determinesthe 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
  • 9.
    code coverage $ pytest--cov -v collected 1 item test_matlib.py::test_div PASSED [100%] ----------- coverage: platform linux, python 3.7.2-final-0 ----------- Name Stmts Miss Cover ------------------------------------ matlib.py 6 2 67% test_matlib.py 4 0 100% ------------------------------------ TOTAL 10 2 80% ========== 1 passed in 0.03 seconds =====
  • 10.
    code coverage def test_div(): assertdiv(4,2) == 2 def div(dividend, divisor): 1 if divisor != 0: 2 result = dividend / divisor 3 else: 4 print('not defined') 5 result = float('NaN') 6 return result this code path is not considered, therefore code coverage of 67% (4/6)
  • 11.
    code coverage testmatlib.py: import math frommatlib import div def test_div(): assert div(4,2) == 2 def test_div_by_zero(): assert math.isnan(div(4,0)) $ pytest --cov -v collected 2 items test_matlib.py::test_div PASSED [ 50%] test_matlib.py::test_div_by_zero PASSED [100%] ----------- coverage: platform linux, python 3.6.6-final-0 ----------- Name Stmts Miss Cover ------------------------------------ matlib.py 6 0 100% test_matlib.py 6 0 100% ------------------------------------ TOTAL 12 0 100%
  • 12.
    branch coverage def my_partial_fn(x): ifx: y = 10 return y def test_my_partial_fn(): assert my_partial_fn(1) == 10 ----------- coverage: platform linux, python 3.6.6-final-0 ----------- Name Stmts Miss Cover ----------------------------------- mylib.py 4 0 100% test_mylib.py 3 0 100% ----------------------------------- TOTAL 7 0 100% code coverage 100%, however, code is bound to fail … >>> my_partial_fn(0) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in my_partial_fn UnboundLocalError: local variable 'y' referenced before assignment
  • 13.
    code coverage there isroom for improvement
  • 15.
    fake tests def unit_under_test: . SendEmail.send_message(msg) . classSendEmail: . . 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 mockto 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 • avoidundesired 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 alibrary 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.
  • 19.
    unittest.mock.patch def get_current_working_path(): path =os.getcwd() return path needs to be replaced within the scope of the unit test @mock.patch('os.getcwd')
  • 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
  • 25.
    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() assert path == '/home/debadmin/mock_call' test condition evaluates to true
  • 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 unittestimport 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
  • 33.
  • 34.
    speaker unit test Ifyou 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