This article demonstrates how to create a unit test for a Python function that raises an exception.1
Problem
Let’s say I have a function set
:
def set(self, k, v):
"""
Set the value of a Setting specified by its key.
:param k: key of the Setting
:type k: str
:param v: value of the Setting
:type v: bool
:return: None
:rtype: None
"""
if not isinstance(v, bool):
raise TypeError(
'\'{}\' is not of type bool.'.format(v))
for setting in self.settings:
if setting.key == k:
setting.value = v
return
else:
raise KeyError(k)
How do I elicit a TypeError
and/or KeyError
. Also, how do I test the
exception message?
Solution
The solution is to use assertRaises()
. assertRaises()
allows an exception to be encapsulated, which means that the test can throw an
exception without exiting execution, as is normally the case for unhandled
exceptions.
There are two ways to use assertRaises()
:
- Using keyword arguments.
- Using a context manager.
The first is the most straightforward:
assertRaises(exception, callable, *args, **kwds)
Simply pass the exception, the callable function, and the parameters of the callable function as keyword arguments that will elicit the exception.
The second is slightly more involved:
assertRaises(exception, msg=None)
Call the function that should raise the exception within a context manager. The context manager will store the caught exception object in its exception attribute. This can be useful if the intention is to perform additional checks on the exception raised.
Here is a unit test that tests for both the KeyError
and TypeError
and
verifies the exception messages raised by the original function:
def test_set(self):
m = MessageSettings(**self.kwargs)
m.set('a', True)
self.assertEqual(MessageSettings(
settings=[
Setting(
key='a',
name='b',
value=True
)]
), m)
self.assertRaises(KeyError, m.set, **{'k': 'x', 'v': True})
with self.assertRaises(TypeError) as context:
m.set('a', 'True')
self.assertEqual(
'\'True\' is not of type bool.', str(context.exception))
I have seen instances where a side_effect
is used in order to simulate an
exception as well. For example:
import unittest
import my_module
class MyTestCase(unittest.TestCase):
def setUp(self):
self.mock_logging = patch.object(
my_module, 'logging', autospec=True).start()
def test(self):
self.mock_logging.info.side_effect = my_module.DeadlineExceededError
...
This pattern is ill-advised in this case, since it couples the test case to the
existence of the logging
module and the explicit call to the patched module
in the function that you are testing. Removing a call to the logging
module
will cause the unit test to fail.
Seven years and 9 minor version updates later, this approach still holds up. However, I use
pytest-raises
now. ↩︎