# 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}]"}
    340 views