How to test that a custom excepthook is installed correctly?

Question:

My app logs unhandled exceptions.

# app.py
import logging
import sys

logger = logging.getLogger(__name__)

def excepthook(exc_type, exc_value, traceback):
    exc_info = exc_type, exc_value, traceback
    if not issubclass(exc_type, (KeyboardInterrupt, SystemExit)):
        logger.error('Unhandled exception', exc_info=exc_info)
    sys.__excepthook__(*exc_info)

sys.excepthook = excepthook

def potato():
    logger.warning('about to die...')
    errorerrorerror

if __name__ == '__main__':
    potato()

These tests pass OK:

# test_app.py
import app
import pytest
import sys
from logging import WARNING, ERROR

def test_potato_raises():
    with pytest.raises(NameError):
        app.potato()

def test_excepthook_is_set():
    assert sys.excepthook is app.excepthook

def test_excepthook_logs(caplog):  
    try:
        whatever
    except NameError as err:
        exc_info = type(err), err, err.__traceback__
    app.excepthook(*exc_info)
    assert caplog.record_tuples == [('app', ERROR, 'Unhandled exception')]
    [record] = caplog.records
    assert record.exc_info == exc_info

But I couldn’t get a test of unhandled exceptions logging working:

def test_unhandled_exceptions_logged(caplog):
    try:
        app.potato()
    finally:
        assert caplog.record_tuples == [
            ('app', WARNING, 'about to die...'),
            ('app', ERROR, 'Unhandled exception'),
        ]
        return  # return eats exception

What’s wrong here? How can we actually trigger the app.excepthook from within a test?

Asked By: wim

||

Answers:

pytest can check the exception message. You can do something like this:

>>> import pytest
>>> def foo():
...  raise ValueError('Unhandled Exception')
...
>>> with pytest.raises(ValueError) as exc:
...  foo()
...
>>> 'Unhandled Exception' in str(exc)
True
>>> str(exc)
'<stdin>:2: ValueError: Unhandled Exception'
>>>

You can actually test the whole thing in one test. No need for multiple test functions.

Python won’t call sys.excepthook until an exception actually propagates all the way through the whole stack and no more code has an opportunity to catch it. It’s one of the very last things that happen before Python shuts down in response to the exception.

As long as your test code is still on the stack, sys.excepthook won’t fire. What little code actually can run after sys.excepthook probably isn’t going to play well with your testing framework. For example, atexit handlers can still run, but the test is over by then. Also, your test framework is probably going to catch the exception itself if you don’t, so sys.excepthook won’t fire anyway.

If you don’t want to call sys.excepthook yourself, your best bet may be to launch an entire subprocess with your excepthook installed and verify the subprocess’s behavior.

from subprocess import Popen, PIPE

def test_app():
    proc = Popen([sys.executable, 'app.py'], stdout=PIPE, stderr=PIPE)
    stdout, stderr = proc.communicate()
    assert proc.returncode == 1
    assert stdout == b''
    assert stderr.startswith(b'about to die...nUnhandled exception')
Answered By: user2357112