David Ye @ Houzz
repr
Strings (Item 75)unittest
Module (Item 76–79)pdb
Interactive Debugging (Item 80)tracemalloc
Understand Memory Usage (Item 81)repr
Stringsrepr
Strings for Debugging Outputprintable representation of an object
my_value = '123'
print(str(my_value)) # 123
print('%s' % my_value) # 123
print(f'{my_value}') # 123
print(format(my_value)) # 123
print(my_value.__format__('s')) # 123
print(my_value.__str__()) # 123
print(f'{my_value!r}') # '123'
repr
of built-in Python types can be passed to eval
to get back the original value:
a = '\x07'
b = eval(repr(a)) # repr(): get the repr string of the input
assert a == b # true
Define the special method __repr__
:
class SomeClass:
def __init__(self, x, y):
self.x = x
self.y = y
obj = SomeClass(1, 'foo')
print(f'{obj!r}') # <__main__.SomeClass object at 0x10963d6d0>
print(obj) # <__main__.SomeClass object at 0x10963d6d0>
class SomeClass:
...
def __repr__(self):
return f'SomeClass({self.x!r}, {self.y!r})'
obj = SomeClass(2, 'bar')
print(f'{obj!r}') # SomeClass(2, 'bar')
print(obj) # SomeClass(2, 'bar')
<__main__.SomeClass object ...>
is the default repr string.
When __str__
is not defined, it will fallback to repr string.
unittest
ModuleBuilt-in Python unit test module.
TestCase
Subclasses
# utils.py
def to_str(data):
if isinstance(data, str):
return data
elif isinstance(data, bytes):
return data.decode('utf-8')
else:
raise TypeError('Must supply str or bytes, '
'found: %r' % data)
# utils.py
from unittest import TestCase, main
from utils import to_str # we want to test this method
class UtilsTestCase(TestCase):
def test_to_str_bytes(self):
self.assertEqual('hello', to_str(b'hello')) # helper method in TestCase class
def test_to_str_str(self):
self.assertEqual('hello', to_str('hello'))
def test_failing(self): # a test will fail!
self.assertEqual('incorrect', to_str('hello'))
if __name__ == '__main__':
main()
with a helpful error message about what went wrong:
$ python3 utils_test.py
F..
===============================================================
FAIL: test_failing (__main__.UtilsTestCase)
---------------------------------------------------------------
Traceback (most recent call last):
File "utils_test.py", line 15, in test_failing
self.assertEqual('incorrect', to_str('hello'))
AssertionError: 'incorrect' != 'hello'
- incorrect
+ hello
Define own helper method:
class MyTestCase(TestCase):
def assertValidTriangle(self, edges):
self.assertEqual(len(edges), 3)
perimeter = sum(edges)
for e in edges:
self.assertGreater(perimeter - e, e)
def test_triangle_generator(self):
my_triangle = TriangleGenerator().generate()
self.assertValidTriangle(my_triangle.edges)
subTest
defining multiple test in a single test method:
from utils import to_str
class DataDrivenTestCase(TestCase):
def test_good(self):
good_cases = [
(b'my bytes', 'my bytes'),
('no error', b'no error'), # This one will fail
('other str', 'other str'),
...
]
for value, expected in good_cases:
with self.subTest(value): # subtests:
self.assertEqual(expected, to_str(value))
setUp
, tearDown
, setUpModule
, and tearDownModule
Test harness: have the test environment set up before test methods can be run
Before / After each test method:
class EnvironmentTest(TestCase):
def setUp(self):
self.test_dir = TemporaryDirectory()
self.test_path = Path(self.test_dir.name)
def tearDown(self):
self.test_dir.cleanup()
def test_modify_file(self):
with open(self.test_path / 'data.bin', 'w') as f:
...
Before / After all test methods in this module:
# for some integration test:
def setUpModule():
createConnection()
def tearDownModule():
closeConnection()
Execution order:
def setUpModule():
print('* Module setup')
def tearDownModule():
print('* Module clean-up')
class MyTest(TestCase):
def setUp(self):
print('* Test setup')
def tearDown(self):
print('* Test clean-up')
def test_end_to_end1(self):
print('* Test 1')
def test_end_to_end2(self):
print('* Test 2')
“* Module setup
* Test setup
* Test 1
* Test clean-up
.* Test setup
* Test 2
* Test clean-up
.* Module clean-up
Mock:
class DatabaseConnection:
...
def get_animals(database, species):
... # query db
from unittest.mock import Mock
mock = Mock(spec=get_animals) # minic the get_animals function
expected = [
('Spot', datetime(2019, 6, 5, 11, 15)),
('Fluffy', datetime(2019, 6, 5, 12, 30)),
('Jojo', datetime(2019, 6, 5, 12, 45)),
]
mock.return_value = expected
database = object()
result = mock(database, 'Jojo')
mock.assert_called_once_with(database, 'Jojo') # pass
mock.assert_called_once_with(database, 'Giraffe') # AssertionError: expected call not found.
from unittest.mock import ANY
mock.assert_called_once_with(database, ANY)
Mock exception:
mock.side_effect = MyError('Boom!')
result = mock(database, 'Jojo') # raise MyError
Patch module / class attribute:
from unittest.mock import patch
with patch('__main__.get_animals'):
print(get_animals) # a MagicMock instance
However, it cannot mock build-in C-extension module:
with patch('datetime.datetime.utcnow'):
datetime.utcnow.return_value = datetime(2019, 6, 5, 15, 45)
# raise TypeError: can't set attributes of built-in/extension type
approach 1: add wrapper
def get_do_rounds_time()
return datetime.utcnow()
with patch('__main__.get_do_rounds_time'):
...
approach 2: reserved kw argument
def do_rounds(database, species, *, utcnow=datetime.utcnow):
now = utcnow()
...
from unittest.mock import DEFAULT
with patch.multiple('__main__', # create multiple mocks in 1 call
autospec=True,
get_food_period=DEFAULT, # create a Mock for this function
get_animals=DEFAULT,
feed_animal=DEFAULT):
now_func = Mock(spec=datetime.utcnow)
now_func.return_value = datetime(2019, 6, 5, 15, 45)
get_food_period.return_value = timedelta(hours=3)
get_animals.return_value = [
...
]
assertions:
result = do_rounds(database, 'Meerkat', utcnow=now_func) # pass the mocked datetime function
assert result == 2
food_func.assert_called_once_with(database, 'Meerkat')
animals_func.assert_called_once_with(database, 'Meerkat')
feed_func.assert_has_calls(
[
call(database, 'Spot', now_func.return_value),
call(database, 'Fluffy', now_func.return_value),
],
any_order=True)
use a wrapper object to encapsulate the database’s interface
class ZooDatabase:
def get_animals(self, species):
...
def get_food_period(self, species):
...
def feed_animal(self, name, when):
before:
def do_rounds(database, species): # a Database connection
feeding_timedelta = get_food_period(database, species)
animals = get_animals(database, species)
...
after:
def do_rounds(database, species): # ZooDatabase
feeding_timedelta = database.get_food_period(species)
animals = database.get_animals(species)
...
DATABASE = None
def get_database():
global DATABASE
if DATABASE is None:
DATABASE = ZooDatabase()
return DATABASE
def main(argv):
database = get_database()
species = argv[1]
count = do_rounds(database, species)
print(f'Fed {count} {species}(s)')
return 0
Ref: item 86: Consider Module-Scoped Code to Configure Deployment Environments
mock ZooDatabase
with patch -> now we can test the main
func:
with patch('__main__.DATABASE', spec=ZooDatabase):
DATABASE.get_food_period.return_value = timedelta(hours=3)
DATABASE.get_animals.return_value = [...]
fake_stdout = io.StringIO()
with contextlib.redirect_stdout(fake_stdout):
main(['program name', 'Meerkat'])
found = fake_stdout.getvalue()
expected = 'Fed 2 Meerkat(s)\n'
assert found == expected
pdb
Interactive Debuggingpdb
pdb
: The Python Debugger
import pdb; pdb.set_trace()
breakpoint()
def some_func:
...
breakpoint() # will enter interactive console here
...
pdb
Commandswhere
: print current call stackup
/ down
move up or down call stackstep
run 1 linenext
run until next line of this functionreturn
run until this function returncontinue
run until next breakpointquit
This enables you to debug a program after it’s already raised an exception and crashed.
$ python -m pdb -c continue main.py
...(some error)
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> postmortem_breakpoint.py(16)compute_rmse()
-> rmse = math.sqrt(mean_err)
(pdb)
or by code: import pdb; pdb.pm()
tracemalloc
Understand Memory Usagetracemalloc
to Understand Memory Usage and Leaksgc.get_objects
: list every object currently known by the garbage collector
import gc
import waste_memory # a custom memory wasting module
found_objects = gc.get_objects()
print('Before:', len(found_objects))
hold_reference = waste_memory.run()
found_objects = gc.get_objects()
print('After: ', len(found_objects))
for obj in found_objects[:3]:
print(repr(obj)[:100])
Before: 6207
After: 16801
<waste_memory.MyObject object at 0x10390aeb8>
<waste_memory.MyObject object at 0x10390aef0>
<waste_memory.MyObject object at 0x10390af28>
However, gc
does not tell how objects were allocated
tracemalloc
take snapshot on memory usage:
import tracemalloc
tracemalloc.start(10) # Set stack depth
time1 = tracemalloc.take_snapshot() # Before snapshot
...
time2 = tracemalloc.take_snapshot() # After snapshot
stats = time2.compare_to(time1, 'lineno') # Compare snapshots
for stat in stats[:3]:
print(stat) # print comparison:
waste_memory.py:5: size=2392 KiB (+2392 KiB), count=29994
➥(+29994), average=82 B
waste_memory.py:10: size=547 KiB (+547 KiB), count=10001
➥(+10001), average=56 B
waste_memory.py:11: size=82.8 KiB (+82.8 KiB), count=100
➥(+100), average=848 B
Thanks for listening!