2020年8月13日 星期四

[ Python 文章收集 ] Testing sys.exit() with pytest

 Source From Here

Preface
When I had tested code which had called sys.exit(), my usual approach was to use mock. First is the target function to test:
mypack/mymodu.py
  1. import sys  
  2.   
  3. def should_exit(rc=1):  
  4.     sys.exit(rc)  
Then is the test case:
tests/test_mymodu.py
  1. import unittest  
  2. from mypack import mymodu  
  3. from unittest import mock  
  4.   
  5. def test_should_exit():  
  6.     with mock.patch.object(mymodu.sys, "exit") as mock_exit:  
  7.         mymodu.should_exit()  
  8.   
  9.     assert mock_exit.call_args[0][0] == 1  
Then you can execute the test case:
# pytest tests/test_mymodu.py
====== test session starts
platform linux -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
rootdir: /tmp/mock_test
collected 1 item

tests/test_mymodu.py . [100%]

====== 1 passed in 0.01s

There are few things I don’t like here:
1. Mocking is always a bit of a black magic, as we stop threating code as black box and start poking inside of it.
2. If sys.exit() is called somewhere else down by the stack, it may be hard to mock. It is impossible to mock code which was loaded dynamically by some kind of plug-in manager.

Any Better Way
All that make me wounder: is there a better way? After some experiments I found a better solution (Asserting about exceptions with pytest.raises):
  1. def test_should_exit_way2():  
  2.     with pytest.raises(SystemExit) as pytest_wrapped_e:  
  3.         mymodu.should_exit(42)  
  4.   
  5.     assert pytest_wrapped_e.type == SystemExit  
  6.     assert pytest_wrapped_e.value.code == 42  
Two key factors:
1. sys.exit() will raise exception called SystemExit, which is NOT inherited from Exception class (Exception and SystemExit both inherited from BaseException).
2. When pytest catches you exception inside it wraps it into own object, providing some additional service. Original exception is stored in value variable, and its type in type.

This example threats testing code as black box. Actual exit my be called anywhere and refactoring should not break test.

Important note: there are two exits: sys.exit() (normal) and os._exit(), which bypasses all python magic and just calls _exit() function for C (man _exit). Later couldn’t be intercepted by catching SystemExit and the single way to debug such code is still to use mock to match os module. Fortunately,os._exit() is an extremely rare occurrence.

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...