# Effective Python 2E
### Chapter 9: Testing and Debugging
[David Ye](https://dwye.dev/) @ Houzz
---
## Outline
- [`repr` Strings (Item 75)](#/2)
- [`unittest` Module (Item 76--79)](#/3)
- [`pdb` Interactive Debugging (Item 80)](#/4)
- [`tracemalloc` Understand Memory Usage (Item 81)](#/5)
---
## `repr` Strings
### Item 75: Use `repr` Strings for Debugging Output
**printable representation** of an object
```python=
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
```
```python=
print(f'{my_value!r}') # '123'
```
<aside class="notes">
一般 print 只會印出值,看不出型態<br />
字串印出 repr 可以知道他是字串
</aside>
----
`repr` of built-in Python types can be passed to `eval` to get back the original value:
```python=
a = '\x07'
b = eval(repr(a)) # repr(): get the repr string of the input
assert a == b # true
```
<aside class="notes">
危險用法
</aside>
----
Define the special method `__repr__`:
```python=
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>
```
```python=
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')
```
#### Note:
`<__main__.SomeClass object ...>` is the default repr string.
When `__str__` is not defined, it will fallback to repr string.
<aside class="notes">
print 是印出 string, 但是如果沒有定義 str,會自動調用 repr(反之未然,因為有預設的 repr)<br />
也能為自定義的 class instance 撰寫他的表示方式,方便 debugging
</aside>
---
## The `unittest` Module
Built-in Python unit test module.
----
### Item 76: Verify Related Behaviors in `TestCase` Subclasses
```python=
# 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)
```
```python=
# 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()
```
<aside class="notes">
可以拿來寫測試,繼承他,使用 helper method 幫處測試
</aside>
----
with a helpful error message about what went wrong:
```bash
$ 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
```
<aside class="notes">
相較內建的 assert,訊息能告訴你 expected 和 actual,更方便 debug
</aside>
----
Define own helper method:
```python=
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)
```
<aside class="notes">
可以自己寫 helper (readable, reusable),例如:利用三角不等式,測試我們產生的三角形是否合法
</aside>
----
`subTest` defining multiple test in a single test method:
```python=
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))
```
<aside class="notes">
可以在一個 method 內跑多組測試,常常被用在這種 iterate 不同 data 的 data driven test
</aside>
----
### Item 77: Isolate Tests from Each Other with `setUp`, `tearDown`, `setUpModule`, and `tearDownModule`
**Test harness**: have the test environment set up before test methods can be run
Before / After each test method:
```python=
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:
```python=
# for some integration test:
def setUpModule():
createConnection()
def tearDownModule():
closeConnection()
```
<aside class="notes">
module 層級的 setup 最常見的就是設立某種 connection(可以用在 integration test)
</aside>
----
Execution order:
```python=
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')
```
```bash
“* Module setup
* Test setup
* Test 1
* Test clean-up
.* Test setup
* Test 2
* Test clean-up
.* Module clean-up
```
<aside class="notes">
執行順序: module setup -> test setup -> test case
</aside>
----
### Item 78: Use Mocks to Test Code with Complex Dependencies
Mock:
- simulate slow / real things. e.g. DB Connection
```python=
class DatabaseConnection:
...
def get_animals(database, species):
... # query db
```
```python=
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
```
```python=
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)
```
<aside class="notes">
避免測試用到 db / db 不穩定等
</aside>
----
Mock exception:
```python=
mock.side_effect = MyError('Boom!')
result = mock(database, 'Jojo') # raise MyError
```
----
Patch module / class attribute:
```python=
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:
```python=
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
```python=
def get_do_rounds_time()
return datetime.utcnow()
with patch('__main__.get_do_rounds_time'):
...
```
<aside class="notes">
Patch 其實有很多用法,也可以用作 decorator,或在 setup 使用,書中沒有詳盡提,可以看官方文件寫得蠻清楚的
</aside>
----
approach 2: reserved kw argument
```python=
def do_rounds(database, species, *, utcnow=datetime.utcnow):
now = utcnow()
...
```
```python=
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:
```python=15
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)
```
<aside class="notes">
這個做法要在原本的 func 開一個 kw arg 位置,我覺得不是很漂亮,但跟上一個比起來也還行 XD <br />
autoSpec -> 自動抓 object 的 attributes? (spec: 參考某個物件的 attr, 避免 mock 時拼錯字之類的)
</aside>
----
### Item 79: Encapsulate Dependencies to Facilitate Mocking and Testing
> use a wrapper object to encapsulate the database’s interface
```python=
class ZooDatabase:
def get_animals(self, species):
...
def get_food_period(self, species):
...
def feed_animal(self, name, when):
```
----
before:
```python=
def do_rounds(database, species): # a Database connection
feeding_timedelta = get_food_period(database, species)
animals = get_animals(database, species)
...
```
after:
```python=
def do_rounds(database, species): # ZooDatabase
feeding_timedelta = database.get_food_period(species)
animals = database.get_animals(species)
...
```
- can create a Mock instance to represent a ZooDatabase
<aside class="notes">
原本還要 patch 好幾個傳入 database 的 methods (get_food_period, get_animals...),現在只要 mock ZooDatabase 就好
</aside>
----
#### write an end-to-end with a mid-level integration test
- still need a way to inject a mock ZooDatabase into the program
- by creating a helper function that acts as a seam for *dependency injection*.
```python=
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*
<aside class="notes">
e2e test 值得 refactor 你的 code,加入 helper,來方便 inject mock,減少大量的模板程式碼做 mock 工作
</aside>
----
mock `ZooDatabase` with patch -> now we can test the `main` func:
```python=16
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 Debugging
### Item 80: Consider Interactive Debugging with `pdb`
`pdb`: The Python Debugger
- was: `import pdb; pdb.set_trace()`
- after 3.7: `breakpoint()`
```python=
def some_func:
...
breakpoint() # will enter interactive console here
...
```
<aside class="notes">
local 測試用的,需要改 code 加入斷點
</aside>
----
#### Common `pdb` Commands
- `where`: print current call stack
- `up` / `down` move up or down call stack
- `step` run 1 line
- `next` run until next line of this function
- `return` run until this function return
- `continue` run until next breakpoint
- `quit`
<aside class="notes">
當然,除了這些 commands 之外,因為這是個 interactive debug console,所以也是可以寫 python code 的,也可以直接輸入變數名稱,拿到其 repr string
</aside>
----
#### post-mortem debugging
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 Usage
### Item 81: Use `tracemalloc` to Understand Memory Usage and Leaks
`gc.get_objects`: list every object currently known by the garbage collector
```python=
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
<aside class="notes">
主要拿來看物件數量
</aside>
----
`tracemalloc` take snapshot on memory usage:
```python=
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
```
<aside class="notes">
可以知道兇手在哪(是誰 allocate 這些 objects)
</aside>
---
# The End
Thanks for listening!
- [back to outline](#/1)
{"metaMigratedAt":"2023-06-17T03:43:45.414Z","metaMigratedFrom":"YAML","title":"Effective Python Chp 9: Testing and Debugging","breaks":true,"slideOptions":"{\"height\":1000,\"width\":1500,\"theme\":\"white\"}","contributors":"[{\"id\":\"915f29e1-3f9c-4908-bbd4-a58795589e48\",\"add\":15502,\"del\":1643}]"}