How often do users behave unexpectedly? More often than not. In most respect, people, systems and the Universe are random.

Maybe your code expects user input or takes data from a received packet and transforms it, perhaps performing complex calculations.

Handling exceptions enables graceful error handling and prevents abrupt program termination.

It allows you to catch and handle unexpected errors, improving the stability and reliability of your code.

Do you raise errors or handle them? And how do you ensure to display the correct and helpful type of error? This is very important, particularly when dealing with User facing tooling like APIs and response codes.

Understanding Exception Testing

Python’s exception handling mechanism allows you to handle various types of exceptions that can occur during program execution, such as ValueError, TypeError, FileNotFoundError, and ZeroDivisionError, among others.

By utilizing try-except blocks, you can gracefully handle exceptions and prevent program crashes.

import os  import math  import re      class InvalidEmailError(Exception):      """      Raised when an email address is invalid      """        pass      def division(a: int | float, b: int | float) -> float | ZeroDivisionError:      """      Returns the result of dividing a by b        Raises:          ZeroDivisionError: If b is 0      """      try:          return a / b      except ZeroDivisionError:          raise ZeroDivisionError("Division by zero is not allowed")      def square_root(a: int) -> float | ValueError:      """      Returns the square root of a        Raises:          ValueError: If a is negative      """      try:          return math.sqrt(a)      except ValueError:          raise ValueError("Square root of negative numbers is not allowed")      def delete_file(filename: str) -> None | FileNotFoundError:      """      Deletes a file        Raises:          FileNotFoundError: If the file does not exist      """      try:          os.remove(filename)      except FileNotFoundError:          raise FileNotFoundError(f"File {filename} not found")      def validate_email(email: str) -> bool | InvalidEmailError:      """      Validates an email address        Raises:          InvalidEmailError: If the email address is invalid      """      if re.match(r"[^@]+@[^@]+\.[^@]+", email):          return True      else:          raise InvalidEmailError("Invalid email address")  

The above code contains 5 functions, each performing a simple task and handling one key exception.

Exceptions can be tested in pytest using the with pytest.raises(EXCEPTION): block syntax.

You then place the code within the block and if your code raises an exception, pytest will pass. Or fail if it doesn’t raise an exception.

To run the tests, simply run pytest in your terminal or you can provide the path to the file as an argument. Use -v for verbose logging.

1pytest -v

pytest-assert-exception-run-test

Matching Assert Exception Messages and Excinfo Object

If you like, you can use the following simple block to assert the exception

12with pytest.raises(FileNotFoundError): delete_file("non_existent_file.txt")

In these examples, we have captured the output in the excinfo object which allows us to assert the actual exception message too.

How To Assert That NO Exception Is Raised

We’ve just seen how to assert when an exception is raised.

But what about the other side of the equation? If we wanna test that No Exception is Raised?

We can do it using Pytest xfail .

def test_division_no_exception_raised():  
    """  
    Test that no exception is raised when the second argument is not 0  
    """  
    try:  
        division(1, 1)  
    except Exception as excinfo:  
        pytest.fail(f"Unexpected exception raised: {excinfo}")  
  
  
def test_square_root_no_exception_raised():  
    """  
    Test that no exception is raised when the argument is not negative  
    """  
    try:  
        square_root(1)  
    except Exception as excinfo:  
        pytest.fail(f"Unexpected exception raised: {excinfo}")

Asserting Custom Exceptions

In Python, we can define our own custom exceptions in the following way.

  
def validate_email(email: str) -> bool | InvalidEmailError:    
    """    
    Validates an email address    
    
    Raises:    
        InvalidEmailError: If the email address is invalid    
    """    
    if re.match(r"[^@]+@[^@]+\.[^@]+", email):    
        return True    
    else:    
        raise InvalidEmailError("Invalid email address")

Multiple Assertions In The Same Test

Maybe you have several conditions you want to check to prove that your test works, Pytest is great at this.

def test_square_root_division_multiple_exceptions():  
    """  
    Test that multiple exceptions can be asserted in a single test  
    """  
    with pytest.raises((ValueError, ZeroDivisionError)) as excinfo:  
        square_root(-1)  
    assert str(excinfo.value) == "Square root of negative numbers is not allowed"