超越基础知识:Pytest和Unittest的高级Python异常测试
![Python异常测试:清晰有效的方法 四海 第1张-四海吧 image by chenspec on pixabay](https://miro.medium.com/v2/resize:fit:640/format:webp/0*9000CefaibzjFlw9.jpg)
测试异常不仅仅是一种形式 – 它是编写可靠代码的重要部分。在本教程中,我们将探索测试引发异常和不引发异常的Python代码的实用和有效方法,验证异常消息的准确性,以及覆盖pytest
和unittest
的参数化测试和非参数化测试。
通过本教程,您将对如何为您的代码编写干净、高效和信息丰富的异常测试有坚实的理解。
让我们看一下以下示例:
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
。
![Python异常测试:清晰有效的方法 四海 第2张-四海吧 作者提供的截图](https://miro.medium.com/v2/resize:fit:640/format:webp/0*M14e05-119iXXeNW.png)
现在我们已经介绍了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的测试栏中提供更清晰和更具信息性的视图,使我们更容易理解和导航测试用例。通过使用这些技术,我们可以改进我们的单元测试,并确保我们的代码正确处理异常。
祝测试愉快!
![Python异常测试:清晰有效的方法 四海 第3张-四海吧 jakob5200在pixabay上的图片](https://miro.medium.com/v2/resize:fit:640/format:webp/0*Js4qcJHyrpHVGHpj.jpg)