Home Artificial Intelligence Python Exception Testing: Clean and Effective Methods

Python Exception Testing: Clean and Effective Methods

5
Python Exception Testing: Clean and Effective Methods

Let’s look into the next example:

def divide(num_1: float, num_2: float) -> float:
if not isinstance(num_1, (int, float))
or not isinstance(num_2, (int, float)):
raise TypeError("a minimum of certainly one of the inputs "
f"shouldn't be a number: {num_1}, {num_2}")

return num_1 / num_2

There are several flows we will test for the function above — blissful flow, a zero denominator, and a non-digit input.

Now, let’s see what such tests would seem like, using pytest:

from contextlib import nullcontext as does_not_raise

import pytest

from operations import divide

def test_happy_flow():
with does_not_raise():
assert divide(30, 2.5) shouldn't be None
assert divide(30, 2.5) == 12.0

def test_division_by_zero():
with pytest.raises(ZeroDivisionError) as exc_info:
divide(10.5, 0)
assert exc_info.value.args[0] == "float division by zero"

def test_not_a_digit():
with pytest.raises(TypeError) as exc_info:
divide("a", 10.5)
assert exc_info.value.args[0] ==
"a minimum of certainly one of the inputs shouldn't be a number: a, 10.5"

We may also perform a sanity check to see what happens once we test an invalid flow against the flawed exception type or once we attempt to envision for a raised exception in a blissful flow. In these cases, the tests will fail:

# Each tests below should fail

def test_wrong_exception():
with pytest.raises(TypeError) as exc_info:
divide(10.5, 0)
assert exc_info.value.args[0] == "float division by zero"

def test_unexpected_exception_in_happy_flow():
with pytest.raises(Exception):
assert divide(30, 2.5) shouldn't be None

So, why did the tests above fail? The with context catches the precise variety of exception requested and verifies that the exception type is indeed the one we asked for.

In test_wrong_exception_check, an exception (ZeroDivisionError) was thrown, but it surely wasn’t caught by TypeError. Subsequently, within the stack trace, we’ll see ZeroDivisionError was thrown and wasn’t caught by the TypeError context.

In test_redundant_exception_context our with pytest.raises context attempted to validate the requested exception type (we provided Exception on this case) but since no exception was thrown — the test failed with the message Failed: DID NOT RAISE .

Now, moving on to the subsequent stage, let’s explore how we will make our tests rather more concise and cleaner by utilizing parametrize.

Parametrize

from contextlib import nullcontext as does_not_raise

import pytest

from operations import divide

@pytest.mark.parametrize(
"num_1, num_2, expected_result, exception, message",
[
(30, 2.5, 12.0, does_not_raise(), None),

(10.5, 0, None, pytest.raises(ZeroDivisionError),
"float division by zero"),

("a", 10.5, None, pytest.raises(TypeError),
"at least one of the inputs is not a number: a, 10.5")

],
ids=["valid inputs",
"divide by zero",
"not a number input"]
)
def test_division(num_1, num_2, expected_result, exception, message):
with exception as e:
result = divide(num_1, num_2)
assert message is None or message in str(e)
if expected_result shouldn't be None:
assert result == expected_result

The ids parameter changes the test-case name displayed on the IDE’s test-bar view. Within the screenshot below we will see it in motion: with ids on the left, and without ids on the best.

screenshot by writer

5 COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here