pytest需用pytest.raises(exceptiongroup)捕获并检查exc_info.value.exceptions,unittest需手动try/except+assertisinstance,嵌套异常宜用递归flatten函数提取所有子异常,且必须用except*而非except捕获子异常。

如何用 pytest 捕获 ExceptionGroup 并断言内部异常
Python 3.11 原生支持 ExceptionGroup,但 pytest 默认不展开它——直接用 pytest.raises(ValueError) 会失败,因为外层是 ExceptionGroup,不是裸的 ValueError。
正确做法是显式匹配 ExceptionGroup 类型,并检查其 exceptions 属性:
def test_raises_group():
with pytest.raises(ExceptionGroup) as exc_info:
raise ExceptionGroup("validation failed", [
ValueError("field 'email' is empty"),
TypeError("field 'age' must be int")
])
assert len(exc_info.value.exceptions) == 2
assert isinstance(exc_info.value.exceptions[0], ValueError)
assert isinstance(exc_info.value.exceptions[1], TypeError)
注意:不要用 match= 参数试图匹配整个组的 message,ExceptionGroup 的 __str__() 行为不稳定,不同 Python 小版本输出格式可能微调。
在 unittest 中 assertRaises 为什么不起作用
unittest.TestCase.assertRaises 在 Python 3.11.6 之前不识别 ExceptionGroup 作为“可接受异常”,即使你写 self.assertRaises(ExceptionGroup),也可能因内部类型检查逻辑跳过匹配。
稳妥方案是手动 try/except + assertIsInstance:
def test_with_unittest(self):
try:
raise ExceptionGroup("oops", [KeyError("missing")])
except ExceptionGroup as e:
self.assertIsInstance(e, ExceptionGroup)
self.assertEqual(len(e.exceptions), 1)
self.assertIsInstance(e.exceptions[0], KeyError)
else:
self.fail("ExceptionGroup not raised")
- 别依赖
assertRaises自动捕获——它对ExceptionGroup的兼容性滞后于语言特性 - 如果项目混合使用
unittest和pytest,建议统一用 pytest,它的raises对ExceptionGroup支持更早、更稳定
嵌套 ExceptionGroup 怎么递归断言
真实场景中(如 asyncio.gather(return_exceptions=True))可能产生多层嵌套的 ExceptionGroup。直接 flatten 所有异常比逐层判断更可靠。
推荐写一个轻量辅助函数:
def all_exceptions_in_group(exc: BaseException) -> list[BaseException]:
if isinstance(exc, ExceptionGroup):
return [e for sub in exc.exceptions for e in all_exceptions_in_group(sub)]
return [exc]
然后在测试里用:
exc_list = all_exceptions_in_group(exc_info.value) assert any(isinstance(e, ConnectionError) for e in exc_list) assert any(isinstance(e, TimeoutError) for e in exc_list)
- 避免用
isinstance(e, ExceptionGroup)多层 if 判断——容易漏掉深层嵌套 - 该函数不修改原异常对象,只读取,安全用于测试断言
为什么 try/except 捕获不到子异常
这是最常被误解的一点:写 except ValueError: 不会捕获 ExceptionGroup 包裹的 ValueError,哪怕它唯一成员就是那个 ValueError。Python 的异常匹配规则只看顶层类型。
必须显式解包或使用 except*(注意是星号,不是普通 except):
try:
raise ExceptionGroup("bad", [ValueError("boom")])
except* ValueError as eg:
print(f"Caught {len(eg.exceptions)} ValueError(s)") # 正确触发
-
except*是 Python 3.11 新语法,不能和普通except混用在同一 try 块中 - 测试时若要验证业务逻辑是否用了
except*,得构造真实调用链,不能只 mock 异常抛出
ExceptionGroup 的测试难点不在语法,而在它改变了异常传播模型——你得同时验证「抛出结构」和「捕获逻辑」两层行为,漏掉任何一层都可能让错误静默通过。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











