
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_raiseimport 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 faildef 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_raiseimport 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.