how to test that an Exception is raised¶
When an error happens in Python, an Exception is raised to break execution of the program, this means nothing past the line that caused it will run.
It is useful because there is a problem to be solved to continue as expected, it can be a pain when it causes the program to stop early. What if I want it to run even with errors? I might want to give messages to the user who does not care about or understand the details of the error.
Exception Handling is a way to deal with this, it allows programs to make decisions when one happens.
test_catching_module_not_found_error_in_tests¶
red: make it fail¶
I open a terminal to run makePythonTdd.sh with
exceptions
as the name of the project./makePythonTdd.sh exceptions
on Windows without Windows Subsystem Linux use makePythonTdd.ps1
./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
(mac) on the keyboard and use the mouse to click ontests/test_exceptions.py:7
to open it in the editorthen change
True
toFalse
to make the test passand change
test_failure
totest_catching_module_not_found_error_in_tests
with an import statementclass TestExceptions(unittest.TestCase): def test_catching_module_not_found_error_in_tests(self): 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
# Exceptions Encountered # AssertionError # ModuleNotFoundError
I can make
does_not_exist.py
to solve the problem but I want to catch or handle it in the test. I add the unittest.TestCase.assertRaises method which checks that the code in its context raises the Exception it is givendef test_catching_module_not_found_error_in_tests(self): with self.assertRaises(ModuleNotFoundError): import does_not_exist
and the test passes
test_catching_name_error_in_tests¶
red: make it fail¶
I add a failing test
def test_catching_name_error_in_tests(self): does_not_exist
and the terminal shows NameError
NameError: name 'does_not_exist' is not defined
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError
green: make it pass¶
I add unittest.TestCase.assertRaises
def test_catching_name_error_in_tests(self):
with self.assertRaises(NameError):
does_not_exist
and the terminal shows passing tests
test_catching_attribute_error_in_tests¶
red: make it fail¶
I add another failing test
def test_catching_attribute_error_in_tests(self): src.exceptions.does_not_exist
the terminal shows NameError
NameError: name 'src' is not defined
I add an import statement for the module
import src.exceptions import unittest
which gives me 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
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError # AttributeError
green: make it pass¶
then I add a call to the unittest.TestCase.assertRaises method
def test_catching_attribute_error_in_tests(self):
with self.assertRaises(AttributeError):
src.exceptions.does_not_exist
and the terminal shows passing tests
test_catching_type_error_in_tests¶
red: make it fail¶
I add a failing test
def test_catching_type_error_in_tests(self): src.exceptions.function_name('argument')
and get AttributeError
AttributeError: module 'src.exceptions' has no attribute 'function_name'
I add the name to
exceptions.py
function_name
and the terminal shows NameError
NameError: name 'function_name' is not defined
then I assign it to None to define it
function_name = None
and get TypeError
TypeError: 'NoneType' object is not callable
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError # AttributeError # TypeError
green: make it pass¶
then I add unittest.TestCase.assertRaises to the test
def test_catching_type_error_in_tests(self):
with self.assertRaises(TypeError):
src.exceptions.function_name('argument')
and the terminal shows passing tests
refactor: make it better¶
If I make function_name
a function
def function_name():
return None
the terminal still shows passing tests, I have the TypeError because the call sends an argument and the function does not accept input. When I add a parameter to the definition
def function_name(argument):
return None
the terminal shows AssertionError
AssertionError: TypeError not raised
because the function call matches the signature. I undo the change to get back the TypeError and make the test pass
def function_name():
return None
test_catching_index_error_in_tests¶
red: make it fail¶
I make a list in a new test
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n']
the first item in it has
0
as its indexdef test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] a_list[0]
the terminal shows green. The index for the last item is the total number of items minus
1
, which is3
in this casedef test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] a_list[3]
still green. When I use a number that is bigger than the index for the last item
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] a_list[4]
the terminal shows IndexError
IndexError: list index out of range
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError # AttributeError # TypeError # IndexError
green: make it pass¶
then I add assertRaises
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4]
and the test passes
I can also index with negative numbers, the one for the last item in the list is
-1
, think reading from right to leftdef test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] 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 casedef test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] a_list[-4]
still green. When I use a negative number that is outside the range
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] a_list[-5]
the terminal shows IndexError
IndexError: list index out of range
I add the assertRaises
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] with self.assertRaises(IndexError): a_list[-5]
and the terminal shows green again
It looks like there is a duplication of the assertRaises but it is not, even though the test is green when I remove the second one
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] a_list[-5]
at the end of the chapter I show why this is not a repetition, I undo the change for now
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] with self.assertRaises(IndexError): a_list[-5]
test_catching_key_error_in_tests¶
red: make it fail¶
I add a dictionary to a new test
def test_catching_key_error_in_tests(self): {'key': 'value'}
when I try to get the value for a key that exists
def test_catching_key_error_in_tests(self): {'key': 'value'}['key']
the terminal shows green and when I use a key that does not exist
def test_catching_key_error_in_tests(self): {'key': 'value'}['does_not_exist']
I get KeyError
KeyError: 'does_not_exist'
another one for the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError # AttributeError # TypeError # IndexError # KeyError
green: make it pass¶
I add assertRaises to the test
def test_catching_key_error_in_tests(self):
with self.assertRaises(KeyError):
{'key': 'value'}['does_not_exist']
and the terminal shows green again
test_catching_zero_division_error_in_tests¶
red: make it fail¶
I add another failing test for something that happened in how to make a calculator
def test_catching_zero_division_error_in_tests(self): 1 / 0
any number divided by
0
and the terminal shows a ZeroDivisionErrorZeroDivisionError: division by zero
I add it to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # NameError # AttributeError # TypeError # IndexError # KeyError # ZeroDivisionError
green: make it pass¶
then add the assertRaises method to the test
def test_catching_zero_division_error_in_tests(self):
with self.assertRaises(ZeroDivisionError):
1 / 0
and the terminal shows passing tests
test_catching_exceptions_in_tests¶
red: make it fail¶
I add a failing test
def test_catching_exceptions_in_tests(self): raise Exception
the terminal shows Exception which is the mother of the Exceptions encountered so far, they inherit from it
Exception
I can use the raise statement to cause any Exception
def test_catching_exceptions_in_tests(self): raise AssertionError
and the terminal will show it
AssertionError
green: make it pass¶
when I add the assertRaises method to the test
def test_catching_exceptions_in_tests(self):
with self.assertRaises(Exception):
raise Exception
the terminal shows all tests are passing.
To review, the assertRaises method checks that the code in its context 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 if you will
def test_catching_key_error_in_tests(self): with self.assertRaises(Exception): {'key': 'value'}['does_not_exist'] def test_catching_zero_division_error_in_tests(self): with self.assertRaises(Exception): 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
I cannot use siblings or cousins to catch other Exceptions
def test_catching_key_error_in_tests(self): with self.assertRaises(ModuleNotFoundError): {'key': 'value'}['does_not_exist']
shows KeyError
KeyError: 'does_not_exist'
because it is not ModuleNotFoundError
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
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] a_list[-5]
If I add a raise statement between the 2 lines
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] raise Exception a_list[-5]
the terminal still shows a passing test, even though Exception is not IndexError, it looks like the assertRaises exits after the first line that causes IndexError. When I move the raise statement above it
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): raise Exception a_list[4] a_list[-5]
the terminal shows Exception
Exception
because it is not IndexError
as a rule of thumb I write one line of code for one how to test that an Exception is raised to know which line caused which error
def test_catching_index_error_in_tests(self): a_list = [1, 2, 3, 'n'] with self.assertRaises(IndexError): a_list[4] with self.assertRaises(IndexError): a_list[-5]
review¶
I have a way to catch Exceptions in tests and ran into the following
Would you like to test handling Exceptions in programs?