Python Testing Exceptions & Edge Cases
Testing exceptions checks that code raises the right error with the right message. Errors are behaviour too.
Check the message, not just the type
The as ctx part keeps the exception, so you can read its message.
import unittest
def divide(a, b):
return a / b
checker = unittest.TestCase()
with checker.assertRaises(ZeroDivisionError) as ctx:
divide(1, 0)
print(str(ctx.exception))Test both sides of a boundary
The rule turns at 18. So test 17, 18, and 19.
def is_adult(age):
return age >= 18
print(is_adult(17), is_adult(18), is_adult(19))File tests with tempfile
The folder exists inside the block. It is gone after the block.
import os
import tempfile
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, "note.txt")
with open(path, "w") as f:
f.write("hi")
print("Inside the block, the file exists:", os.path.exists(path))
print("After the block, the file exists:", os.path.exists(path))Catch a whole family
withdraw is defined in the closing example. Asking for the base class accepts any family member.
with self.assertRaises(BankError):
withdraw(10, 20)Example
import unittest
import tempfile
import os
class BankError(Exception):
pass
class InsufficientFundsError(BankError):
pass
class InvalidAmountError(BankError):
pass
def withdraw(balance, amount):
if amount <= 0:
raise InvalidAmountError("Amount must be positive")
if amount > balance:
raise InsufficientFundsError("Not enough money")
return balance - amount
def save_receipt(folder, text):
path = os.path.join(folder, "receipt.txt")
with open(path, "w") as f:
f.write(text)
return path
class TestWithdraw(unittest.TestCase):
def test_normal_withdraw(self):
self.assertEqual(withdraw(100, 30), 70)
def test_too_much_raises(self):
with self.assertRaises(InsufficientFundsError) as ctx:
withdraw(50, 80)
self.assertEqual(str(ctx.exception), "Not enough money")
def test_zero_amount_raises(self):
with self.assertRaises(InvalidAmountError):
withdraw(50, 0)
def test_exact_balance_works(self):
self.assertEqual(withdraw(50, 50), 0)
def test_receipt_uses_temp_folder(self):
with tempfile.TemporaryDirectory() as folder:
path = save_receipt(folder, "Paid")
with open(path) as f:
self.assertEqual(f.read(), "Paid")
if __name__ == "__main__":
unittest.main()Key points
- Use assertRaises as a context manager to test the code in the with block.
- Save the exception with as ctx and check str(ctx.exception) for the message.
- Boundary value: a value right at the edge of a rule, like age 17 and age 18.
- Test both sides of every boundary.
- Use tempfile.TemporaryDirectory for file tests.
- The folder removes itself afterwards.
- A test for the base class of an exception family catches every member of the family.
- Never print a temp path in a test.
- Testing exceptions means checking both the error type and its message.
In this example, one file holds a small exception family and its tests. BankError is the base class, in the style of the exceptions section. Its two children name the two ways withdraw can refuse. One test checks the message text through ctx.exception. One test checks the exact-balance boundary, which must succeed. The receipt test writes inside a TemporaryDirectory, so no file is left behind. Run the file with python3 -m unittest. The Ran line also reports a time in seconds, which changes on every run. OK means all five tests passed.
Note
The name is random, so the output would change on every run.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Python Playground
Exercise
A rule says a ticket is free for children under 12. Which pair of ages best tests the boundary?