how to test that an Exception is raised


When an error happens in Python, an Exception is raised to stop the program, this means nothing past the line that caused the error will run.

It is useful because there is a problem that must be solved for the program to continue. It is a problem when it causes the program to stop early.

What if I want to test that a program raises an Exception? If the Exception is raised, the test will not continue past the line that caused it.

I can use the try statement to test that some code raises an Exception and continue past the line that caused it.

preview

I have these tests by the end of the chapter

exceptions/tests/test_exceptions.py
 1import src.exceptions
 2import unittest
 3
 4
 5class TestExceptions(unittest.TestCase):
 6
 7    @staticmethod
 8    def assert_raises(code, exception):
 9        try:
10            exec(code)
11        except exception:
12            pass
13        else:
14            raise AssertionError
15
16    def test_catching_module_not_found_error(self):
17        self.assert_raises(
18            'import does_not_exist', ModuleNotFoundError
19        )
20        with self.assertRaises(ModuleNotFoundError):
21            import does_not_exist
exceptions/tests/test_exceptions.py
23    def test_catching_name_error(self):
24        self.assert_raises('not_defined', NameError)
25        with self.assertRaises(NameError):
26            not_defined
27
28    def test_catching_attribute_error(self):
29        self.assert_raises(
30            'src.exceptions.does_not_exist', AttributeError
31        )
32        with self.assertRaises(AttributeError):
33            src.exceptions.does_not_exist
34
35    def test_catching_type_error(self):
36        self.assert_raises(
37            "src.exceptions.function_name('the input')",
38            TypeError
39        )
40        with self.assertRaises(TypeError):
41            src.exceptions.function_name('the input')
exceptions/tests/test_exceptions.py
43    def test_catching_index_error(self):
44        self.assert_raises("'a string'[8]", IndexError)
45        self.assert_raises("'a string'[-9]", IndexError)
46
47        with self.assertRaises(IndexError):
48            'a string'[8]
49        with self.assertRaises(IndexError):
50            'a string'[-9]
51
52        self.assert_raises(
53            "(0, 1, 2, 'n')[100]", IndexError
54        )
55        self.assert_raises(
56            "(0, 1, 2, 'n')[-100]", IndexError
57        )
58
59        with self.assertRaises(IndexError):
60            (0, 1, 2, 'n')[100]
61        with self.assertRaises(IndexError):
62            (0, 1, 2, 'n')[-100]
exceptions/tests/test_exceptions.py
64    def test_catching_key_error(self):
65        self.assert_raises(
66            "{'key': 'value'}['not_in_dictionary']",
67            KeyError
68        )
69        with self.assertRaises(KeyError):
70            {'key': 'value'}['not_in_dictionary']
71
72    def test_catching_zero_division_error(self):
73        self.assert_raises('1 / 0', ZeroDivisionError)
74        with self.assertRaises(ZeroDivisionError):
75            1 / 0
76
77    def test_catching_exceptions(self):
78        self.assert_raises('raise Exception', Exception)
79        with self.assertRaises(Exception):
80            raise Exception
81
82
83# Exceptions seen
84# AssertionError
85# ModuleNotFoundError
86# NameError
87# AttributeError
88# TypeError
89# SyntaxError
90# IndexError
91# KeyError
92# ZeroDivisionError

questions about testing Exceptions


requirements

makePythonTdd


start the project

  • I open a terminal

  • I give exceptions as the first argument to the makePythonTdd program

    ./makePythonTdd.sh exceptions
    
    .\makePythonTdd.ps1 exceptions
    

    the terminal is my friend, and shows AssertionError

    ======================== FAILURES ==========================
    ______________ Testexceptions.test_failure _________________
    
    self = <tests.test_exceptions.Testexceptions testMethod=test_failure>
    
        def test_failure(self):
    >       self.assertFalse(True)
    E       AssertionError: True is not false
    
    tests/test_exceptions.py:7: AssertionError
    ================== short test summary info ==================
    FAILED tests/test_exceptions.py::Testexceptions::test_failure - AssertionError: True is not false
    ===================== 1 failed in X.YZs =====================
    
  • I hold ctrl (Windows/Linux) or option/command (MacOS) on the keyboard and use the mouse to click on tests/test_exceptions.py:7 to put the cursor on line 7

  • I change assertFalse to assertTrue

     4class Testexceptions(unittest.TestCase):
     5
     6    def test_failure(self):
     7        # self.assertFalse(True)
     8        self.assertTrue(True)
     9
    10
    11# Exceptions seen
    

    the test passes.

  • I open a new terminal then change directory to exceptions

    cd exceptions
    
  • I add the new files and folder to git for tracking

    git add .
    
  • I add a git commit message

    git commit -am 'setup project'
    

test_catching_module_not_found_error

ModuleNotFoundError is raised when I try to import a module that does NOT exist.


RED: make it fail


  • I go back to the terminal where the tests are running

  • I change Testexceptions to TestExceptions to match the CapWords format

    1import unittest
    2
    3
    4class TestExceptions(unittest.TestCase):
    5
    6    def test_failure(self):
    
  • I change test_failure to test_catching_module_not_found_error with an import statement in tests/test_exceptions.py

     4class TestExceptions(unittest.TestCase):
     5
     6    def test_catching_module_not_found_error(self):
     7        import does_not_exist
     8
     9
    10# Exceptions seen
    

    the terminal is my friend, and shows ModuleNotFoundError

    ModuleNotFoundError: No module named 'does_not_exist'
    

    I cannot import a module that does not exist. A module is any file that ends in .py

  • I add ModuleNotFoundError to the list of Exceptions seen

    10# Exceptions seen
    11# AssertionError
    12# ModuleNotFoundError
    

I can make a file named does_not_exist.py to solve the problem. What I want to do is catch/handle the Exception in the test to show that import does_not_exist raises ModuleNotFoundError when the file does NOT exist and the test can continue running after confirming the Exception.


how to handle Exceptions

The try statement is like an if statement for Exceptions. It tells the program what to do if an Exception is raised. A simple way to think of it is

  • try something

  • except - if something raises an Exception do something else

If the statement in the try clause raises the Exception in the except clause, Python runs the code in the except block

    try:
┌───┴── do something
└── except Exception:
    └── do something else

If the statement in the try clause block works, Python exits the try statement

    try:
┌───┴── do something
   except Exception:
       do something else

GREEN: make it pass


ModuleNotFoundError is raised when I try to import a module that does NOT exist.


test_catching_name_error

NameError is raised when I try to use a name that is not defined in the file I am working in.


RED: make it fail


  • I add a test for NameError

     9        except ModuleNotFoundError:
    10            pass
    11
    12    def test_catching_name_error(self):
    13        not_defined
    14
    15
    16# Exceptions seen
    

    the terminal is my friend, and shows NameError

    NameError: name 'not_defined' is not defined
    

    because there is no definition for not_defined in tests/test_exceptions.py.

  • I add NameError to the list of Exceptions seen

    16# Exceptions seen
    17# AssertionError
    18# ModuleNotFoundError
    19# NameError
    

GREEN: make it pass


  • I add a try statement to test_catching_name_error to handle NameError

    12    def test_catching_name_error(self):
    13        try:
    14            not_defined
    15        except NameError:
    16            pass
    17
    18
    19# Exceptions seen
    

    the test passes because not_defined raises NameError.

        try:
    ┌───┴── not_defined
    └── except NameError:
        └── pass
    
  • I add a git commit message

    git commit -am \
    'add test_catching_name_error'
    

NameError is raised when I try to use a name that is not defined in the file.


test_catching_attribute_error

AttributeError is raised when I try to get something that does NOT exist from an object that exists.


RED: make it fail


  • I add a test for AttributeError

    15        except NameError:
    16            pass
    17
    18    def test_catching_attribute_error(self):
    19        src.exceptions.does_not_exist
    20
    21
    22# Exceptions seen
    

    the terminal is my friend, and 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 is my friend, and shows AttributeError

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

    src.exceptions.does_not_exist is like an address

    • src is the src folder

    • exceptions is the exceptions folder in the src folder

    • src.exceptions.does_not_exist is pointing to something named does_not_exist in __init__.py in the exceptions folder in the src folder

    the failure happens because Python cannot find does_not_exist in the __init__.py file in the exceptions folder in the src folder. I tried to get something that does NOT exist from an object that exists.

  • I add AttributeError to the list of Exceptions seen

    23# Exceptions seen
    24# AssertionError
    25# ModuleNotFoundError
    26# NameError
    27# AttributeError
    

GREEN: make it pass


  • I add a try statement to test_catching_attribute_error to handle AttributeError

    19    def test_catching_attribute_error(self):
    20        try:
    21            src.exceptions.does_not_exist
    22        except AttributeError:
    23            pass
    24
    25
    26# Exceptions seen
    

    the test passes because src.exceptions.does_not_exist raises AttributeError

        try:
        └── src.exceptions.does_not_exist
            └── src
                └── exceptions
    ┌───────────────┴── __init__.py
    └── except AttributeError:
        └── pass
    
  • I add a git commit message

    git commit -am \
    'add test_catching_attribute_error'
    

AttributeError is raised when I try to get something that does NOT exist from an object that exists.


test_catching_type_error

TypeError is raised when I try to use an object in a way that it cannot be used.


RED: make it fail


  • I add a test for TypeError

    22        except AttributeError:
    23            pass
    24
    25    def test_catching_type_error(self):
    26        src.exceptions.function_name('the input')
    27
    28
    29# Exceptions seen
    

    the terminal is my friend, and shows AttributeError

    AttributeError: module 'src.exceptions'
                    has no attribute 'function_name'
    
  • I open __init__.py from the exceptions folder in the src folder

  • I delete all the text in the file, then add function_name to src/exceptions/__init__.py

    1function_name
    

    the terminal is my friend, and shows NameError

    NameError: name 'function_name' is not defined
    

    because there is no definition for function_name in src/exceptions/__init__.py

  • I point function_name to None to define it

    1function_name = None
    

    the terminal is my friend, and shows TypeError

    TypeError: 'NoneType' object is not callable
    

    a reminder that I cannot call None like a function.

  • I add TypeError to the list of Exceptions seen, in tests/test_exceptions.py

    29# Exceptions seen
    30# AssertionError
    31# ModuleNotFoundError
    32# NameError
    33# AttributeError
    34# TypeError
    

GREEN: make it pass


I add a try statement to test_catching_type_error to handle TypeError

25    def test_catching_type_error(self):
26        try:
27            src.exceptions.function_name('the input')
28        except TypeError:
29            pass
30
31
32# Exceptions seen

the test passes because src.exceptions.function_name('the input') raises TypeError.

    try:
    └── src.exceptions.function_name('the input')
        └── src
            └── exceptions
                └── __init__.py
┌───────────────────┴── function_name = None
└── except TypeError:
    └── pass

REFACTOR: make it better


  • I make function_name a function in src/exceptions/__init__.py

    1def function_name():
    2    return None
    

    the test is still 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_name function does not take input (the parentheses are empty).

  • I add a parameter to the definition

    1def function_name(parameter_name):
    2    return None
    

    the test is still green, because the statement no longer gets to the except block

    try:
    └── src.exceptions.function_name('the input')
        └── src
            └── exceptions
                └── __init__.py
                    └── def function_name(parameter_name):
                        └── return None
    except TypeError:
        pass
    

    I need a better try statement.


how to use try…except…else

The try statement has an else clause that I can use to make a program do something if the Exception in the except clause is not raised

  • I add an else clause to the try statement to raise AssertionError if TypeError is not raised

    25    def test_catching_type_error(self):
    26        try:
    27            src.exceptions.function_name('the input')
    28        except TypeError:
    29            pass
    30        else:
    31            raise AssertionError
    32
    33
    34# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    E           AssertionError
    

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

        try:
    ┌───┴── src.exceptions.function_name('the input')
            └── src
                └── exceptions
                    └── __init__.py
                        └── def function_name(parameter_name):
                            └── return None
       except TypeError:
           pass
    └── else:
        └── raise AssertionError
    
  • I undo the change

    1def function_name():
    2    return
    

    the test is green again.

  • I add a git commit message

    git commit -am \
    'add test_catching_type_error'
    

TypeError is raised when I try to use an object in a way that it cannot be used.


add else clause to try statements


extract assert_raises method

The try statements all look the same, the only differences are the code in the try block and the Exception in the except clause

try:
    code
except Exception:
    pass
else:
    raise AssertionError

RED: make it fail


  • I add a method for the try statement

     5class TestExceptions(unittest.TestCase):
     6
     7    @staticmethod
     8    def assert_raises(code, exception):
     9        try:
    10            code
    11        except exception:
    12            pass
    13        else:
    14            raise AssertionError
    15
    16    def test_catching_module_not_found_error(self):
    
  • I use the assert_raises method in test_catching_module_not_found_error

    16    def test_catching_module_not_found_error(self):
    17        self.assert_raises(
    18            import does_not_exist, ModuleNotFoundError
    19        )
    20        try:
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: invalid syntax
    

    because import does_not_exist runs before it is passed as input to the assert_raises method. I need a way to pass it as a value that will be run inside the assert_raises method not before.

  • I add SyntaxError to the list of Exceptions seen

    52# Exceptions seen
    53# AssertionError
    54# ModuleNotFoundError
    55# NameError
    56# AttributeError
    57# TypeError
    58# SyntaxError
    

GREEN: make it pass


  • I change the import does_not_exist line in test_catching_module_not_found_error to a string

    16    def test_catching_module_not_found_error(self):
    17        self.assert_raises(
    18            'import does_not_exist', ModuleNotFoundError
    19        )
    20        try:
    21            import does_not_exist
    22        except ModuleNotFoundError:
    23            pass
    24        else:
    25            raise AssertionError
    26
    27    def test_catching_name_error(self):
    

    the terminal is my friend, and shows AssertionError

    E           AssertionError
    

    because the string ('import does_not_exist') does not raise ModuleNotFoundError

    assert_raises('import does_not_exist', ModuleNotFoundError)
    └── def assert_raises(code, exception):
        ├── code = 'import does_not_exist'
        ├── exception = ModuleNotFoundError
        └── try:
        ┌───┴── code
           except exception:
               pass
        └── else:
            └── raise AssertionError
    

    I still need a way to run the code in the string inside the assert_raises method.


the exec function

I can use the exec built-in function to run any Python code I pass as a string to it.

  • I add exec to the try block in the assert_raises method

     7    @staticmethod
     8    def assert_raises(code, exception):
     9        try:
    10            exec(code)
    11        except exception:
    12            pass
    13        else:
    14            raise AssertionError
    15
    16    def test_catching_module_not_found_error(self):
    

    the test passes, showing that import does_not_exist raises ModuleNotFoundError which happens when I try to import a module that does NOT exist.

    assert_raises('import does_not_exist', ModuleNotFoundError)
    └── def assert_raises(code, exception):
        ├── code = 'import does_not_exist'
        ├── exception = ModuleNotFoundError
        └── try:
        ┌───┴── exec(code)
        └── except exception:
            └── pass
            else:
                raise AssertionError
    

REFACTOR: make it better


  • I remove the try block from test_catching_module_not_found_error because it is now a repetition of the assert_raises method

    16    def test_catching_module_not_found_error(self):
    17        self.assert_raises(
    18            'import does_not_exist', ModuleNotFoundError
    19        )
    20
    21    def test_catching_name_error(self):
    
  • I use the assert_raises method in test_catching_name_error

    21def test_catching_name_error(self):
    22    self.assert_raises('not_defined', ModuleNotFoundError)
    23    try:
    

    the terminal is my friend, and shows NameError

    NameError: name 'not_defined' is not defined
    

    because NameError is not ModuleNotFoundError or a child of ModuleNotFoundError.

  • I change the expected Exception to NameError

    21    def test_catching_name_error(self):
    22        self.assert_raises('not_defined', NameError)
    23        try:
    24            not_defined
    25        except NameError:
    26            pass
    27        else:
    28            raise AssertionError
    29
    30    def test_catching_attribute_error(self):
    

    the test passes, showing that not_defined raises NameError which happens when I try to use a name that is not defined in the file.

  • I remove the try block from test_catching_name_error because it is now a repetition of the assert_raises method

    21    def test_catching_name_error(self):
    22        self.assert_raises('not_defined', NameError)
    23
    24    def test_catching_attribute_error(self):
    
  • I use the assert_raises method in test_catching_attribute_error

    24    def test_catching_attribute_error(self):
    25        self.assert_raises(
    26            'src.exceptions.does_not_exist', NameError
    27        )
    28        try:
    

    the terminal is my friend, and shows AttributeError

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

    because AttributeError is not NameError or a child of NameError.

  • I change the expected Exception to AttributeError

    24    def test_catching_attribute_error(self):
    25        self.assert_raises(
    26            'src.exceptions.does_not_exist', AttributeError
    27        )
    28        try:
    29            src.exceptions.does_not_exist
    30        except AttributeError:
    31            pass
    32        else:
    33            raise AssertionError
    34
    35    def test_catching_type_error(self):
    

    the test passes, showing that src.exceptions.does_not_exist raises AttributeError which happens when I try to get something that does NOT exist from an object that exists.

  • I remove the try block from test_catching_attribute_error because it is now a repetition of the assert_raises method

    24    def test_catching_attribute_error(self):
    25        self.assert_raises(
    26            'src.exceptions.does_not_exist', AttributeError
    27        )
    28
    29    def test_catching_type_error(self):
    
  • I use the assert_raises method in test_catching_type_error

    29    def test_catching_type_error(self):
    30        self.assert_raises(
    31            "src.exceptions.function_name('the input')",
    32            AttributeError
    33        )
    34        try:
    

    the terminal is my friend, and shows TypeError

    TypeError: function_name() takes
               0 positional arguments but 1 was given
    

    because TypeError is not AttributeError or a child of AttributeError.

  • I change the expected Exception to TypeError

    29    def test_catching_type_error(self):
    30        self.assert_raises(
    31            "src.exceptions.function_name('the input')",
    32            TypeError
    33        )
    34        try:
    35            src.exceptions.function_name('the input')
    36        except TypeError:
    37            pass
    38        else:
    39            raise AssertionError
    40
    41
    42# Exceptions seen
    

    the test passes, showing that src.exceptions.function_name('the input') raises TypeError which happens when I try to use an object in a way that it cannot be used.

  • I remove the try block from test_catching_type_error because it is now a repetition of the assert_raises method

    29    def test_catching_type_error(self):
    30        self.assert_raises(
    31            "src.exceptions.function_name('the input')",
    32            TypeError
    33        )
    34
    35
    36# Exceptions seen
    
  • I add a git commit message

    git commit -am \
    'extract assert_raises method'
    

test_catching_index_error

IndexError is raised when I try to index a list, tuple or string with a number that is

  • bigger than or the same as the number of items in the list, tuple or string

  • smaller than the negative of the number of items in the list, tuple or string


RED: make it fail


  • I add a test for IndexError with a string

    29    def test_catching_type_error(self):
    30        self.assert_raises(
    31            "src.exceptions.function_name('the input')",
    32            TypeError
    33        )
    34
    35    def test_catching_index_error(self):
    36        'a string'[0]
    37
    38
    39# Exceptions seen
    

    the test is still green because the first item in a list, tuple or string has 0 as its index (its position in the container).

  • I change the index from 0 to 7

    35    def test_catching_index_error(self):
    36        'a string'[7]
    37
    38
    39# Exceptions seen
    

    the test is still green because the index for the last item is the total number of items minus 1, which is 7 in this case.

  • If I use a number that is bigger than the index of the last item

    35    def test_catching_index_error(self):
    36        'a string'[8]
    37
    38
    39# Exceptions seen
    

    the terminal is my friend, and shows IndexError

    IndexError: string index out of range
    

    I cannot use a number that is bigger than the index of the last item in a string or that is greater than or equal to the length of the string.

  • I add IndexError to the list of Exceptions seen

    39# Exceptions seen
    40# AssertionError
    41# ModuleNotFoundError
    42# NameError
    43# AttributeError
    44# TypeError
    45# SyntaxError
    46# IndexError
    

GREEN: make it pass


I use the assert_raises method in test_catching_index_error

35    def test_catching_index_error(self):
36        self.assert_raises("'a string'[8]", IndexError)
37
38
39# Exceptions seen
40
41the test passes.

REFACTOR: make it better


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

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        'a string'[-1]
    38
    39
    40# Exceptions seen
    

    the test is still green.

  • I change the index from -1 to -8 for the first item, which is negative the total number of items (like reading from right to left)

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        'a string'[-8]
    38
    39
    40# Exceptions seen
    

    still green.

  • I use a negative number that is smaller than the negative of the number of characters in the string

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        'a string'[-9]
    38
    39
    40# Exceptions seen
    

    the terminal is my friend, and shows IndexError

    IndexError: string index out of range
    
  • I use the assert_raises method to handle the Exception

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39
    40# Exceptions seen
    

    the test is green again. I cannot use a number that is smaller than the negative of the total number of items in the string to index the string.

  • I add a tuple to test_catching_index_error

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        (0, 1, 2, 'n')[1]
    40
    41
    42# Exceptions seen
    

    the test is still green because 1 is the index of the second item.

  • I use a number that is bigger than the number of items in the tuple

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        (0, 1, 2, 'n')[100]
    40
    41
    42# Exceptions seen
    

    the terminal is my friend, and shows IndexError

    IndexError: tuple index out of range
    
  • I use the assert_raises method

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42
    43
    44# Exceptions seen
    

    the test passes.

  • I use a negative number to index the tuple

    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42        (0, 1, 2, 'n')[-2]
    43
    44
    45# Exceptions seen
    

    the test is still green.

  • I use a number that is smaller than the negative of the number of items in the tuple

    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42        (0, 1, 2, 'n')[-100]
    43
    44
    45# Exceptions seen
    

    the terminal is my friend, and shows IndexError

    IndexError: tuple index out of range
    

    I cannot use a number that is smaller than the negative of the number of items in a tuple.

  • I use the assert_raises method to catch the Exception

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42        self.assert_raises(
    43            "(0, 1, 2, 'n')[-100]", IndexError
    44        )
    45
    46    def test_catching_key_error(self):
    

    the test passes.

  • I add a git commit message

    git commit -am \
    'add test_catching_index_error'
    

IndexError is raised when I try to index a list, tuple or string with a number that is

  • bigger than or the same as the number of items in the list, tuple or string.

  • smaller than the negative of the number of items in the list, tuple or string.


test_catching_key_error

KeyError is raised when I try to use a key that is NOT in a dictionary.


RED: make it fail


  • I add a test for KeyError with a dictionary

    42        self.assert_raises(
    43            "(0, 1, 2, 'n')[-100]", IndexError
    44        )
    45
    46    def test_catching_key_error(self):
    47        {'key': 'value'}['key']
    48
    49
    50# Exceptions seen
    

    the test is green because 'key' is a key of the {'key': 'value'} dictionary.

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

    46    def test_catching_key_error(self):
    47        {'key': 'value'}['not_in_dictionary']
    48
    49
    50# Exceptions seen
    

    the terminal is my friend, and shows KeyError

    KeyError: 'not_in_dictionary'
    
  • I add KeyError to the list of Exceptions seen

    50# Exceptions seen
    51# AssertionError
    52# ModuleNotFoundError
    53# NameError
    54# AttributeError
    55# TypeError
    56# SyntaxError
    57# IndexError
    58# KeyError
    

GREEN: make it pass


KeyError is raised when I try to use a key that is NOT in a dictionary.


test_catching_zero_division_error

ZeroDivisionError is raised when I try to divide a number by 0.

RED: make it fail


  • I add a test for ZeroDivisionError

    46    def test_catching_key_error(self):
    47        self.assert_raises(
    48            "{'key': 'value'}['not_in_dictionary']",
    49            KeyError
    50        )
    51
    52    def test_catching_zero_division_error(self):
    53        1 / 0
    54
    55
    56# Exceptions seen
    

    the terminal is my friend, and shows ZeroDivisionError

    ZeroDivisionError: division by zero
    

    because I cannot divide a number by 0.

  • I add ZeroDivisionError to the list of Exceptions seen

    56# Exceptions seen
    57# AssertionError
    58# ModuleNotFoundError
    59# NameError
    60# AttributeError
    61# TypeError
    62# SyntaxError
    63# IndexError
    64# KeyError
    65# ZeroDivisionError
    

GREEN: make it pass


ZeroDivisionError is raised when I try to divide a number by 0.


test_catching_exceptions

how to raise an Exception


I can cause an Exception to happen with the raise statement.


RED: make it fail


  • I add a test with the raise statement

    52    def test_catching_zero_division_error(self):
    53        self.assert_raises('1 / 0', ZeroDivisionError)
    54
    55    def test_catching_exceptions(self):
    56        raise Exception
    57
    58
    59# Exceptions seen
    

    the terminal is my friend, and 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

    55    def test_catching_exceptions(self):
    56        raise AssertionError
    57
    58
    59# Exceptions seen
    

    the terminal shows the Exception I give the raise statement

    AssertionError
    
  • I change the Exception back

    55    def test_catching_exceptions(self):
    56        raise Exception
    57
    58
    59# Exceptions seen
    

    the terminal shows Exception

    Exception
    

GREEN: make it pass


I use the assert_raises method to catch Exception

55    def test_catching_exceptions(self):
56        self.assert_raises('raise Exception', Exception)
57
58
59# Exceptions seen

the test passes.


REFACTOR: make it better


  • I can use Exception to catch any of the Exceptions that inherit from it (its children/subclasses)

    46    def test_catching_key_error(self):
    47        self.assert_raises(
    48            "{'key': 'value'}['not_in_dictionary']",
    49            # KeyError
    50            Exception
    51        )
    52
    53    def test_catching_zero_division_error(self):
    54        # self.assert_raises('1 / 0', ZeroDivisionError)
    55        self.assert_raises('1 / 0', Exception)
    56
    57    def test_catching_exceptions(self):
    

    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 Exception is. It is better to be specific.

    From the Zen of Python: Explicit is better than implicit. I change the Exceptions back.

    46def test_catching_key_error(self):
    47    self.assert_raises(
    48        "{'key': 'value'}['not_in_dictionary']",
    49        KeyError
    50    )
    51
    52def test_catching_zero_division_error(self):
    53    self.assert_raises('1 / 0', ZeroDivisionError)
    54
    55def test_catching_exceptions(self):
    
  • I cannot use sibling or cousin Exceptions to catch other Exceptions

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42        self.assert_raises(
    43            # "(0, 1, 2, 'n')[-100]", IndexError
    44            "(0, 1, 2, 'n')[-100]", ModuleNotFoundError
    45        )
    46
    47    def test_catching_key_error(self):
    

    the terminal is my friend, and shows IndexError

    IndexError: tuple index out of range
    

    because IndexError is not ModuleNotFoundError even though they are both Exceptions.

  • I undo the change

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        self.assert_raises(
    40            "(0, 1, 2, 'n')[100]", IndexError
    41        )
    42        self.assert_raises(
    43            "(0, 1, 2, 'n')[-100]", IndexError
    44        )
    45
    46    def test_catching_key_error(self):
    

    the test is green again.

  • I cannot use a child Exceptions to catch its parent Exception.

    55    def test_catching_exceptions(self):
    56        # self.assert_raises('raise Exception', Exception)
    57        self.assert_raises(
    58            'raise Exception', ZeroDivisionError
    59        )
    60
    61
    62# Exceptions seen
    

    the terminal is my friend, and shows Exception

    Exception
    

    because Exception is not ZeroDivisionError or a child of ZeroDivisionError, even though ZeroDivisionError is an Exception.

  • I undo the change

    55    def test_catching_exceptions(self):
    56        self.assert_raises('raise Exception', Exception)
    57
    58
    59# Exceptions seen
    

    the test is green again.

  • I add a git commit message

    git commit -am 'add test_catching_exceptions'
    

I know how to test that an Exception is raised.


another way to test if an Exception is raised

unittest.TestCase has a method I can use to test if code raises an Exception, it is called assertRaises.

assertRaises checks that the code in its context raises the Exception it is given in parentheses.


  • I add the failing line for test_catching_exceptions

    55    def test_catching_exceptions(self):
    56        self.assert_raises('raise Exception', Exception)
    57        raise Exception
    58
    59
    60# Exceptions seen
    

    the terminal is my friend, and shows Exception.

  • I add assertRaises to handle Exception

    55    def test_catching_exceptions(self):
    56        self.assert_raises('raise Exception', Exception)
    57        with self.assertRaises(Exception):
    58            raise Exception
    59
    60
    61# Exceptions seen
    

    the test passes, showing that assertRaises checks that the code in its context (raise Exception), raises the Exception it is given in parentheses.

  • I add a failing line to test_catching_zero_division_error

    52    def test_catching_zero_division_error(self):
    53        self.assert_raises('1 / 0', ZeroDivisionError)
    54        1 / 0
    55
    56    def test_catching_exceptions(self):
    

    the terminal is my friend, and shows ZeroDivisionError

  • I add assertRaises to handle ZeroDivisionError.

    52    def test_catching_zero_division_error(self):
    53        self.assert_raises('1 / 0', ZeroDivisionError)
    54        with self.assertRaises(ZeroDivisionError):
    55            1 / 0
    56
    57    def test_catching_exceptions(self):
    

    the test passes, showing that assertRaises checks that the code in its context (1 / 0), raises the Exception it is given in parentheses (ZeroDivisionError).

  • I add a failing line to test_catching_key_error

    46    def test_catching_key_error(self):
    47        self.assert_raises(
    48            "{'key': 'value'}['not_in_dictionary']",
    49            KeyError
    50        )
    51        {'key': 'value'}['not_in_dictionary']
    52
    53    def test_catching_zero_division_error(self):
    

    the terminal is my friend, and shows KeyError.

  • I add assertRaises to handle KeyError

    46    def test_catching_key_error(self):
    47        self.assert_raises(
    48            "{'key': 'value'}['not_in_dictionary']",
    49            KeyError
    50        )
    51        with self.assertRaises(KeyError):
    52            {'key': 'value'}['not_in_dictionary']
    53
    54    def test_catching_zero_division_error(self):
    

    the test passes, showing that assertRaises checks that the code in its context ({'key': 'value'}['not_in_dictionary']), raises the Exception it is given in parentheses (KeyError).

  • I add a failure with the assertRaises method to test_catching_index_error

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        with self.assertRaises(ZeroDivisionError):
    40            'a string'[8]
    41
    42        self.assert_raises(
    43            "(0, 1, 2, 'n')[100]", IndexError
    44        )
    

    the terminal is my friend, and shows IndexError.

  • I change ZeroDivisionError to IndexError in test_catching_index_error

    35    def test_catching_index_error(self):
    36        self.assert_raises("'a string'[8]", IndexError)
    37        self.assert_raises("'a string'[-9]", IndexError)
    38
    39        with self.assertRaises(IndexError):
    40            'a string'[8]
    

    the test passes, showing that assertRaises checks that the code in its context ('a string'[8]), raises the Exception it is given in parentheses (IndexError).

  • I add assertRaises for the second statement in test_catching_index_error

    39        with self.assertRaises(IndexError):
    40            'a string'[8]
    41        with self.assertRaises(IndexError):
    42            'a string'[0]
    43
    44        self.assert_raises(
    45            "(0, 1, 2, 'n')[100]", IndexError
    46        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: IndexError not raised
    

    a better error message than the one from my assert_raises method

    AssertionError
    
  • I change the statement to make it raise IndexError

    39        with self.assertRaises(IndexError):
    40            'a string'[8]
    41        with self.assertRaises(IndexError):
    42            'a string'[-9]
    43
    44        self.assert_raises(
    45            "(0, 1, 2, 'n')[100]", IndexError
    46        )
    

    the test passes, showing that assertRaises checks that the code in its context ('a string'[-9]), raises the Exception it is given in parentheses (IndexError).

  • I add assertRaises for the third statement

    52        self.assert_raises(
    53            "(0, 1, 2, 'n')[100]", IndexError
    54        )
    55        self.assert_raises(
    56            "(0, 1, 2, 'n')[-100]", IndexError
    57        )
    58
    59        with self.assertRaises(IndexError):
    60            (0, 1, 2, 'n')[100]
    61
    62    def test_catching_key_error(self):
    

    the test is still green.

  • I add assertRaises for the fourth statement in test_catching_index_error

    59        with self.assertRaises(IndexError):
    60            (0, 1, 2, 'n')[100]
    61        with self.assertRaises(IndexError):
    62            (0, 1, 2, 'n')[-100]
    63
    64    def test_catching_key_error(self):
    

    still green.

  • I use assertRaises in test_catching_type_error

    29    def test_catching_type_error(self):
    30        self.assert_raises(
    31            "src.exceptions.function_name('the input')",
    32            TypeError
    33        )
    34        with self.assertRaises(TypeError):
    35            src.exceptions.function_name('the input')
    36
    37    def test_catching_index_error(self):
    

    green, showing that assertRaises checks that the code in its context (src.exceptions.function_name('the input')), raises the Exception it is given in parentheses (TypeError).

  • I add assertRaises to test_catching_attribute_error

    24    def test_catching_attribute_error(self):
    25        self.assert_raises(
    26            'src.exceptions.does_not_exist', AttributeError
    27        )
    28        with self.assertRaises(AttributeError):
    29            src.exceptions.does_not_exist
    30
    31    def test_catching_type_error(self):
    

    still green, showing that assertRaises checks that the code in its context (src.exceptions.does_not_exist), raises the Exception it is given in parentheses (AttributeError).

  • I use the assertRaises method in test_catching_name_error

    21    def test_catching_name_error(self):
    22        self.assert_raises('not_defined', NameError)
    23        with self.assertRaises(NameError):
    24            not_defined
    25
    26    def test_catching_attribute_error(self):
    

    the test is still green.

  • I use assertRaises in test_catching_module_not_found_error

    16    def test_catching_module_not_found_error(self):
    17        self.assert_raises(
    18            'import does_not_exist', ModuleNotFoundError
    19        )
    20        with self.assertRaises(ModuleNotFoundError):
    21            import src.exceptions
    22
    23    def test_catching_name_error(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: ModuleNotFoundError not raised
    

    because import src.exceptions does not raise ModuleNotFoundError

  • I change the statement

    16    def test_catching_module_not_found_error(self):
    17        self.assert_raises(
    18            'import does_not_exist', ModuleNotFoundError
    19        )
    20        with self.assertRaises(ModuleNotFoundError):
    21            import does_not_exist
    22
    23    def test_catching_name_error(self):
    

    the test passes, showing that assertRaises checks that the code in its context (import does_not_exist), raises the Exception it is given in parentheses (ModuleNotFoundError).

  • I add a git commit message

    git commit -am 'use assertRaises'
    

one exception one exception handler

The assertRaises in test_catching_index_error all catch the same Exception, the only difference is the actual statements that cause IndexError

  • If I remove the second assertRaises

    43    def test_catching_index_error(self):
    44        self.assert_raises("'a string'[8]", IndexError)
    45        self.assert_raises("'a string'[-9]", IndexError)
    46
    47        with self.assertRaises(IndexError):
    48            'a string'[8]
    49        # with self.assertRaises(IndexError):
    50            'a string'[-9]
    

    the test is still green for 'a string'[-9] which should cause IndexError, this makes it look like a second assertRaises is a repetition.

  • If I add a raise statement before 'a string'[-9]

    47        with self.assertRaises(IndexError):
    48            'a string'[8]
    49        # with self.assertRaises(IndexError):
    50            raise Exception
    51            'a string'[-9]
    

    the test is still green, which is not the expected behavior.

  • If I move the raise statement above the first IndexError

    47        with self.assertRaises(IndexError):
    48            raise Exception
    49            'a string'[8]
    50        # with self.assertRaises(IndexError):
    51            'a string'[-9]
    

    the terminal is my friend, and shows Exception

    Exception
    

    because it is NOT IndexError, this is the expected behavior.

  • I undo the changes

    47        with self.assertRaises(IndexError):
    48            'a string'[8]
    49        with self.assertRaises(IndexError):
    50            'a string'[-9]
    

    the test is green again.

As a rule of thumb I write one line of code for one Exception, this way I always know which line caused which Exception.


close the exceptions project

  • I close tests/test_exceptions.py

  • I click in the terminal where the tests are running

  • I use q on the keyboard to leave the tests. The terminal goes back to the command line.

  • I change directory to the parent of exceptions

    cd ..
    

    the terminal is my friend, and shows

    ...\pumping_python
    

    I am back in the pumping_python directory.


review

I ran tests to show that I can use the try statement and assertRaises to catch Exceptions


How many questions can you answer after going through this chapter?


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

Would you like to test handling Exceptions in programs?


rate pumping python

If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too.