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¶
I open a terminal to run makePythonTdd.sh with
exceptionsas the name of the project./makePythonTdd.sh exceptionson Windows without Windows Subsystem for Linux use makePythonTdd.ps1 instead of makePythonTdd.sh
./makePythonTdd.ps1 exceptions
it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError
E AssertionError: True is not false tests/test_exceptions.py:7: AssertionError
I hold
ctrl(Windows/Linux) oroption or command(MacOS) on the keyboard and use the mouse to click ontests/test_exceptions.py:7to open it in the editorthen I change True to False to make the test pass
7 self.assertFalse(False)I change the name of the class to match the CapWords format to follow Python convention
4class TestExceptions(unittest.TestCase):
test_catching_module_not_found_error_in_tests¶
RED: make it fail¶
I change
test_failuretotest_catching_module_not_found_error_in_testswith an import statement1import 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.py10# Exceptions Encountered 11# AssertionError 12# ModuleNotFoundError
I can make
does_not_exist.pyin thesrcfolder (directory) to solve the problem but I want to catch/handle it in the test to show thatimport does_not_existraises ModuleNotFoundError when the file does NOT exist. I add the assertRaises method which checks that the code below it the Exception it is given6 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
NameError: name 'does_not_exist' is not defined
because there is no definition for does_not_exist in
test_exceptions.pyI 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
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.py19# 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.pyfrom thesrcfolder to open it in the editor, then add the name1function_nameNameError: name 'function_name' is not defined
I assign it to None to define it
1function_name = NoneTypeError: 'NoneType' object is not callable
I add it to the list of Exceptions encountered in
test_exceptions.py23# 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_namea function inexceptions.py1def 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 inputwhen 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
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 range
I add it to the list of Exceptions encountered in
test_exceptions.py28# 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 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 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']
KeyError: 'not_in_dictionary'
I add it to the list of Exceptions encountered in
test_exceptions.py34# 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
0the terminal shows ZeroDivisionErrorZeroDivisionError: division by zero
I add it to the list of Exceptions encountered in
test_exceptions.py38# 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
ExceptionException 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
AssertionErrorI change the Exception back
38 def test_catching_exceptions_in_tests(self): 39 raise 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 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 / 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']
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
Exceptionbecause 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):
Exceptionbecause 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