Dmitry Tantsur (Principal Software Engineer, Red Hat)
Using a program to verify correctness of a program (library, module, any piece of software).
Usually white box
Usually black box
Black box
Unit tests do not:
Package layout:
$ find my_utils
my_utils
my_utils/roots.py
my_utils/__init__.py
my_utils/tests
my_utils/tests/__init__.py
my_utils/tests/test_roots.py
import math
def roots(a, b, c):
if not a:
raise ValueError("a cannot be zero")
discriminant = b ** 2 - 4 * a * c
if discriminant < 0:
raise ValueError("discriminant below zero")
return ((- b - math.sqrt(discriminant)) / 2 / a,
(- b + math.sqrt(discriminant)) / 2 / a)
import unittest
from my_utils import roots
class RootsTest(unittest.TestCase):
def test_correct(self):
self.assertEqual(roots.roots(1, -3, 2),
(1.0, 2.0))
def test_negative_a(self):
self.assertRaises(ValueError,
roots.roots, 0, -3, 2)
def test_negative_discriminant(self):
self.assertRaises(ValueError,
roots.roots, 1, 1, 1)
Running only one test file:
$ python3 -m unittest my_utils.tests.test_roots
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Detecting and running all tests:
$ python3 -m unittest discover my_utils
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
And this is how it fails:
$ python3 -m unittest discover my_utils
F..
======================================================================
FAIL: test_correct (tests.test_roots.RootsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/dtantsur/Projects/berlin-python-unittest/my_utils/tests/test_roots.py", line 10, in test_correct
self.assertEqual(roots(1, -3, 2), (1.0, 2.0))
AssertionError: Tuples differ: (1.0, 1.0) != (1.0, 2.0)
First differing element 1:
1.0
2.0
- (1.0, 1.0)
? ^
+ (1.0, 2.0)
? ^
----------------------------------------------------------------------
Ran 3 tests in 0.002s
FAILED (failures=1)
import unittest
from my_utils.roots import roots
Importing the standard unittest package and our code that we plan on testing.
class RootsTest(unittest.TestCase):
def test_correct(self):
# ...
def test_negative_a(self):
# ...
# ...
def test_correct(self):
self.assertEqual(roots.roots(1, -3, 2),
(1.0, 2.0))
Using convenience methods self.assert<***>
def test_negative_a(self):
self.assertRaises(ValueError,
roots.roots, 0, -3, 2)
def test_negative_discriminant(self):
self.assertRaises(ValueError,
roots.roots, 1, 1, 1)
def test_negative_a(self):
self.assertRaisesRegex(ValueError, 'a cannot be zero',
roots.roots, 0, -3, 2)
def test_negative_discriminant(self):
self.assertRaisesRegex(ValueError, 'discriminant below zero',
roots.roots, 1, 1, 1)
assertEqual, assertNotEqual, assertIs, assertIsNot, assertTrue, assertFalse, assertIsInstance, assertNotIsInstance, assertIn, assertNotIn, assertGreater, assertGreaterEqual, assertLess, assertLessEqual, assertRegex, assertNotRegex, assertRaises, assertRaisesRegex, ...
And even more in 3rd party libraries
class RootsTest(unittest.TestCase):
def setUp(self):
# Gets run before every test
def tearDown(self):
# Gets run after every test
Also use addCleanup to clean up after tests.
How to test code that relies on (a lot of) other code?
How to test code that relies on something not available in a regular testing environment?
Python has the mock library:
import sys
def main():
try:
a = int(sys.argv[1])
b = int(sys.argv[2])
c = int(sys.argv[3])
except IndexError:
sys.exit('3 arguments required')
except ValueError:
sys.exit('all arguments must be integers')
print(roots(a, b, c))
if __name__ == '__main__':
main()
$ python3 -m my_utils.roots
3 arguments required
$ python3 -m my_utils.roots a b c
all arguments must be integers
$ python3 -m my_utils.roots 1 4 4
(-2.0, -2.0)
Problems:
The unittest.mock.patch function allows to replace an object temporary while a test is running and restore it back.
Can be used in many forms, we will only consider some of them.
from unittest import mock
class MainTest(unittest.TestCase):
@mock.patch('sys.argv', [None, '1', '-3', '2'])
def test_correct(self):
roots.main()
Using patch as decorator to automate patching object and restoring it after the test.
Rough equivalent:
class MainTest(unittest.TestCase):
def test_correct(self):
import sys
old_argv = sys.argv
sys.argv = [None, '1', '-3', '2']
try:
roots.main() # our actual test
finally:
sys.argv = old_argv
This test does not verify that main function does anything.
To verify that printing is done correctly, we need to replace the print function with something that will track calls.
Magic objects that allow any operations on them and record them for future verification.
m = mock.Mock()
r = m.abc(42, cat='meow')
assert isinstance(m.abc, mock.Mock)
assert isinstance(r, mock.Mock)
m.abc.assert_called_once_with(42, cat='meow')
assert r is m.abc.return_value
class MainTest(unittest.TestCase):
@mock.patch('sys.argv', [None, '1', '-3', '2'])
@mock.patch('builtins.print')
def test_correct(self, mock_print):
roots.main()
mock_print.assert_called_once_with((1.0, 2.0))
If we make a mistake, the test will tell us:
$ python3 -m unittest discover my_utils
F...
======================================================================
FAIL: test_correct (tests.test_roots.MainTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib64/python3.6/unittest/mock.py", line 1179, in patched
return func(*args, **keywargs)
File "/home/dtantsur/Projects/berlin-python-unittest/my_utils/tests/test_roots.py", line 27, in test_correct
mock_print.assert_called_once_with((2.0, 2.0))
File "/usr/lib64/python3.6/unittest/mock.py", line 825, in assert_called_once_with
return self.assert_called_with(*args, **kwargs)
File "/usr/lib64/python3.6/unittest/mock.py", line 814, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: print((2.0, 2.0))
Actual call: print((1.0, 2.0))
----------------------------------------------------------------------
Ran 4 tests in 0.002s
FAILED (failures=1)
Handling sys.exit:
We can make it raise an exception!
@mock.patch('builtins.print')
class MainTest(unittest.TestCase):
@mock.patch('sys.argv', [None, '1', '-3', '2'])
def test_correct(self, mock_print):
roots.main()
mock_print.assert_called_once_with((1.0, 2.0))
@mock.patch('sys.exit')
@mock.patch('sys.argv', [None, '1', '-3'])
def test_missing_argument(self, mock_exit, mock_print):
mock_exit.side_effect = RuntimeError
self.assertRaises(RuntimeError, roots.main)
mock_exit.assert_called_once_with(
'3 arguments required')
mock_print.assert_not_called()
mock.patch:
Mock:
Next part: owlet.today/talks/berlin-python-unittest-2