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
start the project
I open a terminal
I type pwd to make sure I am in the
pumping_pythonfolderpwdthe terminal goes back to the command line
.../pumping_pythonNote
if you are not in the
pumping_pythonfolder, trycd ~/pumping_pythonI name this project
exceptionsI open
makePythonTdd.shormakePythonTdd.ps1in the editorTip
Here is a quick way to open
makePythonTdd.shormakePythonTdd.ps1if you are using Visual Studio Codecode makePythonTdd.shon Windows without Windows Subsystem for Linux use
code makePythonTdd.ps1I change everywhere I have
more_magicinmakePythonTdd.shormakePythonTdd.ps1to the name of this project1#!/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.pyI run the program in the terminal
./makePythonTdd.shAttention
on Windows without Windows Subsystem for Linux use
makePythonTdd.ps1NOTmakePythonTdd.sh./makePythonTdd.ps1the 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:7to open it in the editorthen 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# ModuleNotFoundErrorI can make
does_not_exist.pyin thesrcfolder (directory) to solve the problem but I want to catch/handle it in the test. This way I can show thatimport does_not_existraises ModuleNotFoundError when the file does NOT exist. I add the assertRaises method6 def test_catching_module_not_found_error_in_tests(self): 7 with self.assertRaises(ModuleNotFoundError): 8 import does_not_existthe 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_existNameError: name 'does_not_exist' is not definedbecause there is no definition for does_not_exist in
test_exceptions.pyI 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_existNameError: name 'src' is not definedI add an import statement at the top of the file for the module
1import src.exceptions 2import unittestthe terminal shows AttributeError
AttributeError: module 'src.exceptions' has no attribute 'does_not_exist'src.exceptions.does_not_existis like an addresssrcis thesrcfolderexceptionsisexceptions.pyin thesrcfoldersrc.exceptions.does_not_existis pointing to something nameddoes_not_existinexceptions.pyin thesrcfolder
the failure happened because Python cannot find
does_not_existinexceptions.pyin thesrcfolder. tried to get something that does NOT exist from something that existsI 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.pyfrom thesrcfolder in the editor, then add the name1function_nameNameError: name 'function_name' is not definedI point it to None to define it
1function_name = NoneTypeError: 'NoneType' object is not callableI add TypeError to the list of Exceptions seen in
test_exceptions.py23# 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_namea function inexceptions.py1def function_name(): 2 return Nonethe 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 inputwhen I add a parameter to the definition
1def function_name(parameter_name): 2 return Nonethe terminal shows AssertionError
AssertionError: TypeError not raisedbecause TypeError is NOT raised since the function call matches the definition. I undo the change
1def function_name(): 2 return Nonethe 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
0as its index23 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 is3in this case23 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 rangeI 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 left23 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,
-4in this case23 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 rangeI 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
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']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 / 0the terminal shows ZeroDivisionError
ZeroDivisionError: division by zeroI cannot divide a number by
0I 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 ExceptionExceptionException 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 AssertionErrorand the terminal shows the Exception I give the raise statement
AssertionErrorI change the Exception back
38 def test_catching_exceptions_in_tests(self): 39 raise ExceptionException
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 / 0all 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 implicit34 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 / 0I 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']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 ExceptionExceptionbecause 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 Exceptionthe 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):Exceptionbecause 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.pyin the editorI 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_pythondirectory
Note
on Windows without Windows Subsystem for Linux
the terminal shows
(.venv) ...\pumping_python\type_errorI deactivate the virtual environment
deactivatethe terminal goes back to the command line,
(.venv)is no longer on the left side...\pumping_python\exceptionsI change directory to the parent of
exceptionscd ..the terminal shows
...\pumping_pythonI am back in the
pumping_pythondirectory
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
what is next?
you know
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