Testing sys.exit() with pytest

George Shuklin
Python Pandemonium
Published in
1 min readJun 12, 2017

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:

  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.

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:

  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 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.

--

--

George Shuklin
Python Pandemonium

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.