pythonpython-3.xunit-testingpython-unittestassertraises

Custom exceptions in unittests


I have created my custom exceptions as such within errors.py

mapper = {
    'E101':
    'There is no data at all for these constraints',
    'E102':
    'There is no data for these constraints in this market, try changing market',
    'E103':
    'There is no data for these constraints during these dates, try changing dates',
}


class DataException(Exception):
    def __init__(self, code):
        super().__init__()
        self.msg = mapper[code]

    def __str__(self):
        return self.msg

Another function somewhere else in the code raises different instances of DataException if there is not enough data in a pandas dataframe. I want to use unittest to ensure that it returns the appropriate exception with its corresponding message.

Using a simple example, why does this not work:

from .. import DataException
def foobar():
    raise DataException('E101')

import unittest
with unittest.TestCase.assertRaises(DataException):
    foobar()

As suggested here: Python assertRaises on user-defined exceptions

I get this error:

TypeError: assertRaises() missing 1 required positional argument: 'expected_exception'

Or alternatively:

def foobar():
    raise DataException('E101')

import unittest
unittest.TestCase.assertRaises(DataException, foobar)

results in:

TypeError: assertRaises() arg 1 must be an exception type or tuple of exception types

Why is it not recognizing DataException as an Exception? Why does the linked stackoverflow question answer work without supplying a second argument to assertRaises?


Solution

  • You are trying to use methods of the TestCase class without creating an instance; those methods are not designed to be used in that manner.

    unittest.TestCase.assertRaises is an unbound method. You'd use it in a test method on a TestCase class you define:

    class DemoTestCase(unittest.TestCase):
        def test_foobar(self):
            with self.assertRaises(DataException):
                foobar()
    

    The error is raised because unbound methods do not get self passed in. Because unittest.TestCase.assertRaises expects both self and a second argument named expected_exception you get an exception as DataException is passed in as the value for self.

    You do now have to use a test runner to manage your test cases; add

    if __name__ == '__main__':
        unittest.main()
    

    at the bottom and run your file as a script. Your test cases are then auto-discovered and executed.

    It is technically possible to use the assertions outside such an environment, see Is there a way to use Python unit test assertions outside of a TestCase?, but I recommend you stick to creating test cases instead.

    To further verify the codes and message on the raised exception, assign the value returned when entering the context to a new name with with ... as <target>:; the context manager object captures the raised exception so you can make assertions about it:

    with self.assertRaises(DataException) as context:
        foobar()
    
    self.assertEqual(context.exception.code, 'E101')
    self.assertEqual(
        context.exception.msg,
        'There is no data at all for these constraints')
    

    See the TestCase.assertRaises() documentation.

    Last but not least, consider using subclasses of DataException rather than use separate error codes. That way your API users can just catch one of those subclasses to handle a specific error code, rather than having to do additional tests for the code and re-raise if a specific code should not have been handled there.