Testing sys.exit() with pytest
When I had tested code which had called sys.exit()
, my usual approach was to use mock:
def test_exit(mymodule):
with mock.patch.object(mymodule.sys, "exit") as mock_exit:
mymodule.should_exit()
assert mock_exit.call_args[0][0] == 42
There are few things I don’t like here:
- Mocking is always a bit of a black magic, as we stop threating code as black box and start poking inside of it.
- 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.
All that make me wounder: is there a better way?
After some experiments I found a better solution.
def test_exit(mymodule):
with pytest.raises(SystemExit) as pytest_wrapped_e:
mymodule.will_exit_somewhere_down_the_stack()
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 42
Two key factors:
sys.exit
will raise exception calledSystemExit
, which is NOT inherited fromException
class (Exception
andSystemExit
both inherited fromBaseException
).- 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 intype
.
This example threats code you test 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.