Exception Testing#
Expected Exceptions#
Use pytest.raises when an exception is the expected result of an operation:
import pytest
with pytest.raises(ValueError):
validate_quantity(0)
The test fails if the expected exception is not raised.
Use the narrowest appropriate exception type. Testing against a broad
Exception can allow an unrelated failure to satisfy the test.
Inspecting the Exception#
The context-manager form can expose the captured exception:
with pytest.raises(OverfillError) as exc_info:
apply_fill(order, fill)
assert exc_info.value.order_id == "ORD-001"
exc_info is a pytest ExceptionInfo object rather than the exception
itself.
The actual exception object is available through:
exc_info.value
For several assertions, assigning the exception to a descriptive local name is often clearer:
with pytest.raises(OverfillError) as exc_info:
apply_fill(order, fill)
error = exc_info.value
assert error.order_id == "ORD-001"
assert error.filled_quantity == 0
assert error.incoming_quantity == 200
assert error.original_quantity == 100
The assignment is only for readability. Accessing exc_info.value directly
has the same meaning.
Control Flow#
Normal exception and context-manager control flow still applies inside
pytest.raises.
Consider:
with pytest.raises(OverfillError) as exc_info:
apply_fill(order, fill)
assert exc_info.value.order_id == "ORD-001"
When apply_fill raises the expected exception:
normal execution of the
withsuite stops immediately;pytest.raiseshandles the expected exception;the
withstatement completes;execution continues after the
withstatement.
Code after the raising operation inside the same with suite is therefore
not executed:
with pytest.raises(OverfillError) as exc_info:
apply_fill(order, fill)
# Unreachable when apply_fill() raises.
assert exc_info.value.order_id == "ORD-001"
Assertions about the captured exception should normally be placed after the
with block.
Structured Exception Assertions#
Domain exceptions can carry structured diagnostic information:
with pytest.raises(OverfillError) as exc_info:
apply_fill(order, fill)
error = exc_info.value
assert error.order_id == "ORD-001"
assert error.filled_quantity == 0
assert error.incoming_quantity == 200
assert error.original_quantity == 100
Prefer asserting stable domain data when those attributes are part of the exception contract.
The message can also be tested:
assert "would overfill order" in str(error)
When only the message needs to be matched, pytest.raises supports
match:
with pytest.raises(
OverfillError,
match="would overfill order",
):
apply_fill(order, fill)
Exception Type Matching#
pytest.raises accepts the specified exception type and its subclasses.
For example:
class TradeError(Exception):
pass
class OverfillError(TradeError):
pass
with pytest.raises(TradeError):
raise OverfillError()
This passes because OverfillError is a subclass of TradeError.
Prefer testing the most specific exception that represents the intended contract unless the abstraction being tested deliberately exposes the base exception type.
Interview Notes#
pytest.raises verifies that an operation fails in the expected way.
In context-manager form:
the expected exception interrupts the remaining statements in the block;
pytest handles that exception;
execution continues after the block;
exc_info.valuecontains the actual exception object.
The important distinction is between an exception being raised and an exception remaining unhandled. An unhandled exception terminates the current normal execution path; a handled exception can allow execution to continue at the appropriate point.