how to test that an Exception is raised


When an Exception is raised, it stops the program from running. I can use the assertRaises method from the unittest.TestCase class to test that some code raises an Exception.

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

preview

These 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

questions about testing Exceptions

Here are questions you can answer after going through this chapter


requirements

makePythonTdd.sh or makePythonTdd.ps1


start the project

  • I open a terminal

  • I type pwd to make sure I am in the pumping_python folder

    pwd
    

    the terminal goes back to the command line

    .../pumping_python
    

    Note

    if you are not in the pumping_python folder, try cd ~/pumping_python

  • I name this project exceptions

  • I open makePythonTdd.sh or makePythonTdd.ps1 in the editor

    Tip

    Here is a quick way to open makePythonTdd.sh or makePythonTdd.ps1 if you are using Visual Studio Code

    code makePythonTdd.sh
    

    on Windows without Windows Subsystem for Linux use

    code makePythonTdd.ps1
    
  • I change everywhere I have more_magic in makePythonTdd.sh or makePythonTdd.ps1 to the name of this project

     1#!/bin/bash
     2mkdir exceptions
     3cd exceptions
     4mkdir src
     5touch src/exceptions.py
     6mkdir tests
     7touch tests/__init__.py
     8
     9echo "import unittest
    10
    11
    12class TestExceptions(unittest.TestCase):
    13
    14    def test_failure(self):
    15        self.assertFalse(True)
    16
    17
    18# Exceptions seen
    19# AssertionError
    20" > tests/test_exceptions.py
    
  • I run the program in the terminal

    ./makePythonTdd.sh
    

    Attention

    on Windows without Windows Subsystem for Linux use makePythonTdd.ps1 NOT makePythonTdd.sh

    ./makePythonTdd.ps1
    

    the terminal 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 open it in the editor

  • then I change True to False in the assertion

    7        self.assertFalse(False)
    

    the test passes


test_catching_module_not_found_error_in_tests

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


RED: make it fail


I change test_failure to test_catching_module_not_found_error_in_tests with an import statement in test_exceptions.py

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

the terminal 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


GREEN: make it pass


  • I add ModuleNotFoundError to the list of Exceptions seen

    10# Exceptions seen
    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. This way I can show that import does_not_exist raises ModuleNotFoundError when the file does NOT exist. I add the assertRaises method

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

    the test passes.

assertRaises checks that the code in its context, raises the Exception it is given in parentheses. ModuleNotFoundError is raised when I try to import a module that does NOT exist


test_catching_name_error_in_tests

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


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 NameError to the list of Exceptions seen

    14# Exceptions seen
    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, showing that NameError is raised when I use a name that is not defined in the file


test_catching_attribute_error_in_tests

AttributeError is raised when I try to call something that does NOT exist from something that does exist


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'
    

    src.exceptions.does_not_exist is like an address

    • src is the src folder

    • exceptions is exceptions.py in the src folder

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

    the failure happened because Python cannot find does_not_exist in exceptions.py in the src folder. tried to get something that does NOT exist from something that exists

  • I add the AttributeError to the list of Exceptions seen

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

GREEN: make it pass


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. AttributeError is raised when I try to call something that does NOT exist from something that does exist


test_catching_type_error_in_tests

TypeError is raised when I call something in a way that it should NOT be called


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 in the editor, then add the name

    1function_name
    

    the terminal shows NameError

    NameError: name 'function_name' is not defined
    

    I point it to None to define it

    1function_name = None
    

    the terminal 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 test_exceptions.py

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

GREEN: make it pass


I use assertRaises to take care of the Exception

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(parameter_name):
    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

TypeError is raised when I call something in a way that it should NOT be called


test_catching_index_error_in_tests

IndexError is raised when I try to index a list with a number that is

  • bigger than or the same as the number of items in the lists

  • smaller than the negative of the number of items in the list


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 cannot use a number that is bigger than the index of the last item in a list or that is greater than or equal to the length of the list

  • I add IndexError to the list of Exceptions seen

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

GREEN: make it pass


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, like 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. I cannot use a number that is smaller than the negative of the total number of items in the list to index the list

  • 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]
    

IndexError is raised when I try to index a list with a number that is

  • bigger than or the same as the number of items in the lists

  • smaller than the negative of the number of items in the list


test_catching_key_error_in_tests

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


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 of 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 KeyError to the list of Exceptions seen

    34# Exceptions seen
    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. KeyError is raised when I try to use a key that is NOT in a dictionary

test_catching_zero_division_error_in_tests


RED: make it fail


  • I add another failing test

    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
    

    the terminal shows ZeroDivisionError

    ZeroDivisionError: division by zero
    

    I cannot divide a number by 0

  • I add ZeroDivisionError to the list of Exceptions seen

    38# Exceptions seen
    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. ZeroDivisionError is raised when I try to divide any number by 0, same as I get undefined when I try it with a calculator, because dividing by 0 is underfined in Mathematics, I can use this with test_division in the calculator project


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

    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 to catch it

38    def test_catching_exceptions_in_tests(self):
39        with self.assertRaises(Exception):
40            raise Exception
41
42
43# Exceptions seen

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


REFACTOR: make it better


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

    34    def test_catching_key_error_in_tests(self):
    35        with self.assertRaises(Exception):
    36            {'key': 'value'}['not_in_dictionary']
    37
    38    def test_catching_zero_division_error_in_tests(self):
    39        with self.assertRaises(Exception):
    40            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

    34    def test_catching_key_error_in_tests(self):
    35        with self.assertRaises(KeyError):
    36            {'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


one exception one exception handler

  • 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, which is NOT the expected behavior. Exception is not IndexError and still 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

  • I remove the failing line and put the assertRaises back in the right place

    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. 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 test_exceptions.py in the editor

  • I click in the terminal and exit the tests with ctrl+c on the keyboard, the terminal shows

    …/pumping_python

    I am back in the pumping_python directory

Note

on Windows without Windows Subsystem for Linux

  • the terminal shows

    (.venv) ...\pumping_python\type_error
    
  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    ...\pumping_python\exceptions
    
  • I change directory to the parent of exceptions

    cd ..
    

    the terminal shows

    ...\pumping_python
    

    I am back in the pumping_python directory


review

I can use assertRaises to catch Exceptions in tests and tested the following


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?

you know

Would you like to test handling Exceptions in programs?


rate pumping python

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