how to test that an Exception is raised

preview

Here are the tests I have by the end of the chapter

 1import src.exceptions
 2import unittest
 3
 4
 5class TestExceptions(unittest.TestCase):
 6
 7    def test_catching_module_not_found_error_in_tests(self):
 8        with self.assertRaises(ModuleNotFoundError):
 9            import does_not_exist
10
11    def test_catching_name_error_in_tests(self):
12        with self.assertRaises(NameError):
13            does_not_exist
14
15    def test_catching_attribute_error_in_tests(self):
16        with self.assertRaises(AttributeError):
17            src.exceptions.does_not_exist
18
19    def test_catching_type_error_in_tests(self):
20        with self.assertRaises(TypeError):
21            src.exceptions.function_name('the_input')
22
23    def test_catching_index_error_in_tests(self):
24        a_list = [1, 2, 3, 'n']
25        with self.assertRaises(IndexError):
26            a_list[4]
27        with self.assertRaises(IndexError):
28            a_list[-5]
29
30    def test_catching_key_error_in_tests(self):
31        with self.assertRaises(KeyError):
32            {'key': 'value'}['does_not_exist']
33
34    def test_catching_zero_division_error_in_tests(self):
35        with self.assertRaises(ZeroDivisionError):
36            1 / 0
37
38    def test_catching_exceptions_in_tests(self):
39        with self.assertRaises(Exception):
40            raise Exception
41
42    def test_catching_exceptions_w_messages(self):
43        with self.assertRaisesRegex(
44            Exception, 'BOOM!'
45        ):
46            src.exceptions.raise_exception()
47
48    def test_catching_failure(self):
49        self.assertEqual(
50            src.exceptions.an_exception_handler(
51                src.exceptions.raise_exception
52            ),
53            'failed'
54        )
55
56    def test_catching_success(self):
57        self.assertEqual(
58            src.exceptions.an_exception_handler(
59                src.exceptions.does_not_raise_exception
60            ),
61            'succeeded'
62        )
63
64
65# Exceptions Encountered
66# AssertionError
67# ModuleNotFoundError
68# NameError
69# AttributeError
70# TypeError
71# IndexError
72# KeyError
73# ZeroDivisionError

requirements

test_catching_module_not_found_error_in_tests

RED: make it fail

  • I change test_failure to test_catching_module_not_found_error_in_tests with an import statement

    1import unittest
    2
    3
    4class TestExceptions(unittest.TestCase):
    5
    6    def test_catching_module_not_found_error_in_tests(self):
    7        import does_not_exist
    

    the terminal shows ModuleNotFoundError

    ModuleNotFoundError: No module named 'does_not_exist'
    

GREEN: make it pass

  • I add it to the list of Exceptions encountered in test_exceptions.py

    10# Exceptions Encountered
    11# AssertionError
    12# ModuleNotFoundError
    
  • I can make does_not_exist.py in the src folder (directory) to solve the problem but I want to catch/handle it in the test to show that import does_not_exist raises ModuleNotFoundError when the file does NOT exist. I add the assertRaises method which checks that the code below it the Exception it is given

    6    def test_catching_module_not_found_error_in_tests(self):
    7        with self.assertRaises(ModuleNotFoundError):
    8            import does_not_exist
    

    the test passes


test_catching_name_error_in_tests

RED: make it fail

  • I add another failing test

     6    def test_catching_module_not_found_error_in_tests(self):
     7        with self.assertRaises(ModuleNotFoundError):
     8            import does_not_exist
     9
    10    def test_catching_name_error_in_tests(self):
    11        does_not_exist
    

    the terminal shows NameError

    NameError: name 'does_not_exist' is not defined
    

    because there is no definition for does_not_exist in test_exceptions.py

  • I add it to the list of Exceptions encountered

    14# Exceptions Encountered
    15# AssertionError
    16# ModuleNotFoundError
    17# NameError
    

GREEN: make it pass

I add assertRaises

10      def test_catching_name_error_in_tests(self):
11          with self.assertRaises(NameError):
12              does_not_exist

the test passes


test_catching_attribute_error_in_tests

RED: make it fail

  • I add another failing test, this time for AttributeError

    10    def test_catching_name_error_in_tests(self):
    11        with self.assertRaises(NameError):
    12            does_not_exist
    13
    14    def test_catching_attribute_error_in_tests(self):
    15        src.exceptions.does_not_exist
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    
  • I add an import statement at the top of the file for the module

    1import src.exceptions
    2import unittest
    

    the terminal shows AttributeError

    AttributeError: module 'src.exceptions' has no attribute 'does_not_exist'
    

    because I tried to get something that does NOT exist from something that exists

  • I add the error to the list of Exceptions encountered in test_exceptions.py

    19# Exceptions Encountered
    20# AssertionError
    21# ModuleNotFoundError
    22# NameError
    23# AttributeError
    

GREEN: make it pass

then I add the assertRaises method

15    def test_catching_attribute_error_in_tests(self):
16        with self.assertRaises(AttributeError):
17            src.exceptions.does_not_exist

the test passes


test_catching_type_error_in_tests

RED: make it fail

  • I add a failing test for TypeError

    15    def test_catching_attribute_error_in_tests(self):
    16        with self.assertRaises(AttributeError):
    17            src.exceptions.does_not_exist
    18
    19    def test_catching_type_error_in_tests(self):
    20        src.exceptions.function_name('the_input')
    

    the terminal shows AttributeError

    AttributeError: module 'src.exceptions' has no attribute 'function_name'
    
  • I open exceptions.py from the src folder to open it in the editor, then add the name

    1function_name
    

    the terminal shows NameError

    NameError: name 'function_name' is not defined
    

    I assign it to None to define it

    1function_name = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • I add it to the list of Exceptions encountered in test_exceptions.py

    23# Exceptions Encountered
    24# AssertionError
    25# ModuleNotFoundError
    26# NameError
    27# AttributeError
    28# TypeError
    

GREEN: make it pass

then I add assertRaises to the test

19    def test_catching_type_error_in_tests(self):
20        with self.assertRaises(TypeError):
21            src.exceptions.function_name('the_input')

the test passes

REFACTOR: make it better

  • when I make function_name a function in exceptions.py

    1def function_name():
    2    return None
    

    the terminal still shows green because TypeError is raised since the call from the test - src.exceptions.function_name('the_input') sends 'the_input' as input and the function does not take input

  • when I add a parameter to the definition

    1def function_name(the_input):
    2    return None
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    

    because TypeError is NOT raised since the function call matches the definition. I undo the change

    1def function_name():
    2    return None
    

    the terminal shows green again


test_catching_index_error_in_tests

RED: make it fail

  • I want to test catching IndexError, I add a new test with a list

    19    def test_catching_type_error_in_tests(self):
    20        with self.assertRaises(TypeError):
    21            src.exceptions.function_name('the_input')
    22
    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    

    the first item in a list has 0 as its index

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        a_list[0]
    

    the terminal shows green. The index for the last item is the total number of items minus 1, which is 3 in this case

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        a_list[3]
    

    still green. When I use a number that is bigger than the index for the last item

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        a_list[4]
    

    the terminal shows IndexError

    IndexError: list index out of range
    
  • I add it to the list of Exceptions encountered in test_exceptions.py

    28# Exceptions Encountered
    29# AssertionError
    30# ModuleNotFoundError
    31# NameError
    32# AttributeError
    33# TypeError
    34# IndexError
    

GREEN: make it pass

  • then I add assertRaises

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    

    the test passes

REFACTOR: make it better

  • I can also index with negative numbers, the one for the last item in the list is -1, think reading from right to left

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        a_list[-1]
    

    the terminal still shows passing tests. The index for the first item is negative the total number of items, -4 in this case

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        a_list[-4]
    

    still green. When I use a negative number that is outside the range

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        a_list[-5]
    

    the terminal shows IndexError

    IndexError: list index out of range
    

    I add assertRaises

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        with self.assertRaises(IndexError):
    28            a_list[-5]
    

    the terminal shows green again

  • It looks like this is a duplication of the assertRaises but it is not, even though the test is green when I remove the second one

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27            a_list[-5]
    28        # with self.assertRaises(IndexError):
    

    I show why this is not a repetition at the end of the chapter. I undo the change for now

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        with self.assertRaises(IndexError):
    28            a_list[-5]
    

test_catching_key_error_in_tests

RED: make it fail

  • I add a dictionary to a new test for KeyError

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        with self.assertRaises(IndexError):
    28            a_list[-5]
    29
    30    def test_catching_key_error_in_tests(self):
    31        {'key': 'value'}
    
  • when I try to get the value for a key that is in the dictionary

    30    def test_catching_key_error_in_tests(self):
    31        {'key': 'value'}['key']
    

    the terminal shows green

  • when I use a key that is NOT in the dictionary

    30    def test_catching_key_error_in_tests(self):
    31        {'key': 'value'}['not_in_dictionary']
    

    the terminal shows KeyError

    KeyError: 'not_in_dictionary'
    
  • I add it to the list of Exceptions encountered in test_exceptions.py

    34# Exceptions Encountered
    35# AssertionError
    36# ModuleNotFoundError
    37# NameError
    38# AttributeError
    39# TypeError
    40# IndexError
    41# KeyError
    

GREEN: make it pass

I add assertRaises to the test

30    def test_catching_key_error_in_tests(self):
31        with self.assertRaises(KeyError):
32            {'key': 'value'}['not_in_dictionary']

the test passes

test_catching_zero_division_error_in_tests

RED: make it fail

  • I add another failing test, this time for the Exception that happened in how to make a calculator when testing division

    30    def test_catching_key_error_in_tests(self):
    31        with self.assertRaises(KeyError):
    32            {'key': 'value'}['not_in_dictionary']
    33
    34    def test_catching_zero_division_error_in_tests(self):
    35        1 / 0
    

    any number divided by 0 the terminal shows ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • I add it to the list of Exceptions encountered in test_exceptions.py

    38# Exceptions Encountered
    39# AssertionError
    40# ModuleNotFoundError
    41# NameError
    42# AttributeError
    43# TypeError
    44# IndexError
    45# KeyError
    46# ZeroDivisionError
    

GREEN: make it pass

I add assertRaises

34    def test_catching_zero_division_error_in_tests(self):
35        with self.assertRaises(ZeroDivisionError):
36            1 / 0

the test passes


test_catching_exceptions_in_tests

RED: make it fail

  • I add a failing test with the raise statement

    34    def test_catching_zero_division_error_in_tests(self):
    35        with self.assertRaises(ZeroDivisionError):
    36            1 / 0
    37
    38    def test_catching_exceptions_in_tests(self):
    39        raise Exception
    

    the terminal shows Exception

    Exception
    

    Exception is the mother of all the Exceptions covered so far, they inherit from it

  • I can use the raise statement to cause any Exception I want intentionally

    38    def test_catching_exceptions_in_tests(self):
    39        raise AssertionError
    

    and the terminal shows the Exception I give the raise statement

    AssertionError
    

    I change the Exception back

    38    def test_catching_exceptions_in_tests(self):
    39        raise Exception
    

    the terminal shows Exception

    Exception
    

GREEN: make it pass

I add the assertRaises method

38    def test_catching_exceptions_in_tests(self):
39        with self.assertRaises(Exception):
40            raise Exception

the terminal shows all tests are passing. The assertRaises method checks that the code under it raises the Exception it is given.

REFACTOR: make it better

  • I can use Exception to catch any of the Exceptions that inherit from it - its children

    30    def test_catching_key_error_in_tests(self):
    31        with self.assertRaises(Exception):
    32            {'key': 'value'}['not_in_dictionary']
    33
    34    def test_catching_zero_division_error_in_tests(self):
    35        with self.assertRaises(Exception):
    36            1 / 0
    

    all the tests are still green.

    The problem with using Exception to catch its children, is it does not tell anyone that reads the code what the actual error is or which line caused it, especially when there is more than one line of code in the context.

    It is better to be specific, from the Zen of Python: Explicit is better than implicit

    34    def test_catching_key_error_in_tests(self):
    35        with self.assertRaises(KeyError):
    36            {'key': 'value'}['not_in_dictionary']
    37
    38    def test_catching_zero_division_error_in_tests(self):
    39        with self.assertRaises(ZeroDivisionError):
    40            1 / 0
    
  • I cannot use sibling or cousin Exceptions to catch other Exceptions

    34    def test_catching_key_error_in_tests(self):
    35        with self.assertRaises(ModuleNotFoundError):
    36            {'key': 'value'}['not_in_dictionary']
    

    the terminal shows KeyError

    KeyError: 'not_in_dictionary'
    

    because it is not ModuleNotFoundError even though they are both Exceptions. I undo the change

        def test_catching_key_error_in_tests(self):
            with self.assertRaises(KeyError):
                {'key': 'value'}['not_in_dictionary']
    

    the test passes

  • I cannot use children Exceptions to catch parent Exceptions

    38    def test_catching_exceptions_in_tests(self):
    39        with self.assertRaises(ZeroDivisionError):
    40            raise Exception
    

    the terminal shows Exception

    Exception
    

    because it is not ZeroDivisionError even though it is an Exception. I undo the change

    38    def test_catching_exceptions_in_tests(self):
    39        with self.assertRaises(Exception):
    40            raise Exception
    

    the test passes

  • As promised here is why the second AssertRaises in test_catching_index_error_in_tests is not a repetition, even though the test still passes when I remove it

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27            a_list[-5]
    28        # with self.assertRaises(IndexError):
    

    If I add a raise statement between the 2 lines

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27            raise Exception
    28            a_list[-5]
    29        # with self.assertRaises(IndexError):
    

    the terminal still shows green, even though Exception is not IndexError, it does NOT get raised. The assertRaises exits after the first line that causes IndexError and does NOT run the other lines.

    When I move the raise statement above the first IndexError

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            raise Exception
    27            a_list[4]
    28            a_list[-5]
    29        # with self.assertRaises(IndexError):
    

    the terminal shows Exception

    Exception
    

    because it is NOT IndexError, this is the expected behavior

  • as a rule of thumb I write one line of code for one Exception, this way I always know exactly which line caused which Exception

    23    def test_catching_index_error_in_tests(self):
    24        a_list = [1, 2, 3, 'n']
    25        with self.assertRaises(IndexError):
    26            a_list[4]
    27        with self.assertRaises(IndexError):
    28            a_list[-5]
    

    all tests are passing! I know how to test that an Exception is raised


review

I have a way to catch Exceptions in tests and tested the following

Would you like to test handling Exceptions in programs?


Click Here to see the code from this chapter