Testing for Exceptions

When writing unit tests, we need to test not only for correct effects of successful calls but also to test error checking. In particular we need to validate if passing invalid data raises appropriate exceptions.

With stdlib unittest such code looks like this:

self.assertRaises(ValueError, int, 'abc')

I prefer nose which exports a similar helper function from nose.tools module. The code is almost the same:

assert_raises(ValueError, int, 'abc')

However, sometimes we need to check if the exception is raised when accessing an attribute or attempting to delete a key, for example. These operations have specialized syntax which cannot be used directly with assert_raises, so it is common to use closures for such tests:

assert_raises(AttributeError, lambda: ob.attr)
def _test_delkey():
    del d['key']
assert_raises(KeyError, _test_delkey)

This can also be done with getattr or special methods:

assert_raises(AttributeError, getattr, ob, 'attr')
assert_raises(KeyError, d.__delitem__, 'key')

But there’s a better way

Context managers (with syntax) were around since Python 2.5 and have one little-known feature that we can use for our testing helper. The feature I’m talking about is their ability to inspect and prevent some exceptions from propagating from their scope. This is accomplished by returning True from the __exit__ method.

There’s a helper in mext.test just like that. Here’s the implementation:

import sys

class expect_exc(object):
    def __init__(self, etype):
        self.etype = etype

    def __enter__(self):
        pass

    def __exit__(self, etype, exc, tb):
        if not etype:
            raise AssertionError("Expected %s, raised nothing" % self.etype.__name__)
        elif not isinstance(exc, self.etype):
            raise AssertionError("Expected %s, raised %r" % (self.etype.__name__, exc))
        else:
            sys.exc_clear()
            return True

And here’s what the tests above can look like with expect_exc:

with expect_exc(AttributeError):
    ob.attr

with expect_exc(KeyError):
    del d['key']

If you can drop Python 2.4 compatibility in your tests, the clarity this approach gives you is well worth it.