Home Artificial Intelligence Python Exception Testing: Clean and Effective Methods pytest unittest

Python Exception Testing: Clean and Effective Methods pytest unittest

0
Python Exception Testing: Clean and Effective Methods
pytest
unittest

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("no less than one in all the inputs "
f"is just not a number: {num_1}, {num_2}")

return num_1 / num_2

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

Now, let’s see what such tests would appear 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) is just not 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] ==
"no less than one in all the inputs is just not a number: a, 10.5"

We may also perform a sanity check to see what happens once we test an invalid flow against the incorrect exception type or once we attempt to envision for a raised exception in a glad 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) is just not 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, nevertheless it wasn’t caught by TypeError. Due to this fact, 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 are able to 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 is just not 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 are able to see it in motion: with ids on the left, and without ids on the suitable.

screenshot by writer

LEAVE A REPLY

Please enter your comment!
Please enter your name here