Press "Enter" to skip to content

Python异常测试:清晰有效的方法

超越基础知识:Pytest和Unittest的高级Python异常测试

image by chenspec on pixabay

测试异常不仅仅是一种形式 – 它是编写可靠代码的重要部分。在本教程中,我们将探索测试引发异常和不引发异常的Python代码的实用和有效方法,验证异常消息的准确性,以及覆盖pytestunittest的参数化测试和非参数化测试。

通过本教程,您将对如何为您的代码编写干净、高效和信息丰富的异常测试有坚实的理解。

让我们看一下以下示例:

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("至少一个输入不是数字:{num_1},{num_2}")        return num_1 / num_2

我们可以对上述函数进行几种流程的测试 – 正常流程、零作为除数的情况以及非数字输入。

现在,让我们看一下使用pytest的这些测试会是什么样子:

pytest

from contextlib import nullcontext as does_not_raiseimport pytestfrom operations import dividedef test_happy_flow():    with does_not_raise():        assert divide(30, 2.5) is not None    assert divide(30, 2.5) == 12.0def 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,10.5"

我们还可以进行一次合理性检查,看看当我们针对错误的异常类型进行无效流程测试时会发生什么,或者当我们试图在正常流程中检查引发的异常时会发生什么。在这些情况下,测试将失败:

# 下面的两个测试都应该失败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 not None

那么,为什么上面的测试失败了呢?with上下文捕获了所请求的特定类型的异常,并验证异常类型确实是我们要求的那个。

test_wrong_exception中,引发了一个异常(ZeroDivisionError),但它没有被TypeError捕获。因此,在堆栈跟踪中,我们将看到引发了ZeroDivisionError,并且TypeError上下文未捕获该异常。

test_unexpected_exception_in_happy_flow中,我们的with pytest.raises上下文试图验证所请求的异常类型(在这种情况下,我们提供了Exception),但由于没有引发异常 – 测试失败,消息为Failed: DID NOT RAISE <class ‘Exception’>

现在,进入下一个阶段,让我们探索如何通过使用parametrize使我们的测试更加简洁和清晰。

Parametrize

from contextlib import nullcontext as does_not_raiseimport pytestfrom 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),         "至少一个输入不是数字:a,10.5")    ],    ids=["有效输入",         "除以零",         "非数字输入"])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 not None:        assert result == expected_result

参数ids会改变在IDE的测试栏视图中显示的测试用例名称。在下面的截图中,我们可以看到它的作用:左边是使用ids,右边是没有使用ids

作者提供的截图

现在我们已经介绍了pytest框架,让我们看看如何使用unittest编写相同的测试用例。

unittest

from unittest import TestCasefrom operations import divideclass TestDivide(TestCase):    def test_happy_flow(self):        result = divide(0, 10.5)        self.assertEqual(result, 0)    def test_division_by_zero(self):        with self.assertRaises(ZeroDivisionError) as context:            divide(10, 0)        self.assertEqual(context.exception.args[0], "division by zero")    def test_not_a_digit(self):        with self.assertRaises(TypeError) as context:            divide(10, "c")        self.assertEqual(context.exception.args[0],                         "at least one of the inputs "                         "is not a number: 10, c")

如果我们想在unittest中使用parameterized,我们需要安装这个包。让我们看看在unittest中使用参数化测试的样例:

参数化

import unittestfrom parameterized import parameterized  # 需要安装from operations import dividedef get_test_case_name(testcase_func, _, param):    test_name = param.args[-1]    return f"{testcase_func.__name__}_{test_name}"class TestDivision(unittest.TestCase):    @parameterized.expand([        (30, 2.5, 12.0, None, None, "valid inputs"),        (10.5, 0, None, ZeroDivisionError,         "float division by zero", "divide by zero"),        ("a", 10.5, None, TypeError,         "at least one of the inputs is not a number: a, 10.5",         "not a number input")    ], name_func=get_test_case_name)    def test_division(self, num_1, num_2, expected_result, exception_type,                      exception_message, test_name):        with self.subTest(num_1=num_1, num_2=num_2):            if exception_type is not None:                with self.assertRaises(exception_type) as e:                    divide(num_1, num_2)                self.assertEqual(str(e.exception), exception_message)            else:                result = divide(num_1, num_2)                self.assertIsNotNone(result)                self.assertEqual(result, expected_result)

unittest中,我们也修改了测试用例的名称,类似于上面的pytest的示例。然而,为了实现这一点,我们使用了name_func参数以及一个自定义的函数。

总结一下,今天我们探讨了测试Python异常的有效方法。我们学习了如何识别预期抛出的异常,并验证异常消息是否与我们的预期相符。我们探讨了多种测试divide函数的方法,包括使用传统的pytest方法和更简洁的parametrize方法。我们还探索了在unittest中使用parameterized的等价方法,这需要安装该库,也可以在没有安装的情况下使用。使用ids和自定义测试名称可以在IDE的测试栏中提供更清晰和更具信息性的视图,使我们更容易理解和导航测试用例。通过使用这些技术,我们可以改进我们的单元测试,并确保我们的代码正确处理异常。

祝测试愉快!

jakob5200在pixabay上的图片
Leave a Reply

Your email address will not be published. Required fields are marked *