Testing exceptions, warnings and logs

unittest · Essentials · 9 min

Error paths are where bugs hide, and they need tests as much as the happy path.

To assert that code raises, use assertRaises as a context manager:

with self.assertRaises(ValueError):
    parse_age("-3")

The test fails if no exception, or a different one, is raised. Capture the exception to inspect it: with self.assertRaises(ValueError) as cm: then check cm.exception. assertRaisesRegex(ValueError, "negative") also matches the message with a regex search.

Keep the with block small: only the call that should raise. If setup code inside the block raises the same exception type by accident, the test passes for the wrong reason.

Related helpers with the same shape:

  • assertWarns(DeprecationWarning) and assertWarnsRegex check warnings.
  • assertLogs("myapp", level="WARNING") captures log records emitted on a logger (and its children); cm.output holds formatted lines such as 'WARNING:myapp:disk low'. The test fails if nothing is logged.
  • assertNoLogs (3.10+) asserts that nothing is logged.

Exception-group code can be tested by catching ExceptionGroup and inspecting .exceptions.

Example

import logging, unittest, warnings

def parse_age(text: str) -> int:
    age = int(text)
    if age < 0:
        raise ValueError(f"negative age: {age}")
    if age > 150:
        logging.getLogger("app").warning("suspicious age %d", age)
    return age

class TestParseAge(unittest.TestCase):
    def test_negative(self):
        with self.assertRaises(ValueError) as cm:
            parse_age("-3")
        self.assertIn("negative", str(cm.exception))

    def test_not_a_number(self):
        self.assertRaisesRegex(ValueError, "invalid literal", parse_age, "x")

    def test_logs_suspicious(self):
        with self.assertLogs("app", level="WARNING") as cm:
            parse_age("200")
        self.assertEqual(cm.output, ["WARNING:app:suspicious age 200"])

unittest.main(exit=False)           # ... Ran 3 tests ... OK

Remember

  • with self.assertRaises(Exc) as cm: checks an exception; cm.exception holds it
  • assertRaisesRegex also matches the message
  • assertWarns, assertLogs and assertNoLogs cover warnings and logging

Watch out for

  • Putting several statements in an assertRaises block, so the wrong line raising still passes the test.

Check yourself

  1. A test uses with self.assertRaises(KeyError): and the block raises nothing. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises