
在复杂的应用程序中,自定义异常是处理特定错误情况、提供清晰错误信息和实现优雅错误恢复的关键机制。它们允许开发者定义业务逻辑中特有的错误类型,从而使代码更具可读性和可维护性。
以一个API调用场景为例,我们可以定义一个ApiException来封装HTTP请求失败的详细信息:
import inspect
class ApiException(Exception):
"""
自定义API异常类,封装HTTP错误码、消息和调用位置信息。
"""
def __init__(self, response) -> None:
self.http_code = response.status_code
self.message = response.text.replace("\n", " ")
# 获取异常抛出时的调用栈信息
self.caller = inspect.getouterframes(inspect.currentframe(), 2)[1]
self.caller_file = self.caller[1]
self.caller_line = self.caller[2]
def __str__(self) -> str:
return f"Error code {self.http_code} with message '{self.message}' in file {self.caller_file} line {self.caller_line}"
# 模拟API响应对象
class MockResponse:
def __init__(self, ok, status_code, text):
self.ok = ok
self.status_code = status_code
self.text = text
# 模拟API调用逻辑
def call_gitlab_api(response: MockResponse):
if response.ok:
# 假设这里返回一个MergeRequest对象
return {"status": "success"}
else:
raise ApiException(response=response)对这些自定义异常进行单元测试,可以确保当特定条件触发时,程序能够抛出正确的异常类型,并且异常中包含的错误信息是准确和完整的。
在测试中,我们通常会期望使用isinstance(err, MyException)来验证捕获到的异常是否为我们预期的类型。然而,在某些复杂的测试环境或模块加载机制下,即使type(err)显示的是正确的类名和模块路径,isinstance()仍然可能返回False。
立即学习“Python免费学习笔记(深入)”;
例如,原始问题中描述的现象:
# 假设这是单元测试中的一段代码
try:
call_gitlab_api(MockResponse(ok=False, status_code=401, text="Unauthorized"))
assert False # 如果没有抛出异常,则测试失败
except Exception as err:
# TestLogger.info(type(err)) # 打印结果可能是 <class 'APIs.api_exceptions.ApiException'>
# TestLogger.info(isinstance(err, ApiException)) # 却可能打印 False
assert isinstance(err, ApiException) # 导致测试失败这种现象通常是由于Python解释器在不同的上下文(例如,在测试运行器重新加载模块时)加载了相同名称但实际上是不同的类对象。即使它们的名称、模块路径完全相同,isinstance()或is运算符在比较时,会认为它们是不同的类型。虽然这种情况不常见,但一旦发生,调试起来会比较棘手。
为了避免上述isinstance()可能带来的困惑,并确保异常测试的可靠性,我们推荐以下几种策略。
这是最Pythonic且最可靠的异常测试方法。通过在except子句中直接指定要捕获的异常类型,Python解释器会负责精确匹配异常的类型,包括其继承关系。
import unittest
# 假设ApiException和call_gitlab_api已定义在可导入的模块中
# from your_module import ApiException, call_gitlab_api, MockResponse
class TestApiExceptionHandling(unittest.TestCase):
def test_api_call_raises_api_exception(self):
"""
测试当API响应不成功时,是否抛出ApiException。
"""
mock_response = MockResponse(ok=False, status_code=401, text="Unauthorized access")
try:
call_gitlab_api(mock_response)
self.fail("ApiException was not raised as expected.") # 如果没有抛出异常,强制测试失败
except ApiException as err:
# 验证异常类型已经通过except子句完成
# 进一步验证异常的属性,确保其内容正确
self.assertEqual(err.http_code, 401)
self.assertIn("Unauthorized access", err.message)
# 也可以验证其他属性,如caller_file, caller_line等
except Exception as err:
self.fail(f"Caught an unexpected exception type: {type(err).__name__}")
def test_api_call_succeeds(self):
"""
测试当API响应成功时,不抛出异常并返回正确结果。
"""
mock_response = MockResponse(ok=True, status_code=200, text='{"status": "success"}')
result = call_gitlab_api(mock_response)
self.assertEqual(result, {"status": "success"})
# 运行测试
# if __name__ == '__main__':
# unittest.main()优点:
尽管存在潜在问题,isinstance()在大多数标准场景下仍然是有效的。如果您的测试环境简单,没有复杂的模块加载或重载机制,它通常会正常工作。当您需要在一个通用的except Exception as err:块中处理多种异常类型时,isinstance()可以用于区分它们。
import unittest
class TestApiExceptionHandlingWithIsinstance(unittest.TestCase):
def test_api_call_raises_api_exception_with_isinstance(self):
"""
测试当API响应不成功时,使用isinstance验证是否抛出ApiException。
"""
mock_response = MockResponse(ok=False, status_code=403, text="Forbidden")
try:
call_gitlab_api(mock_response)
self.fail("ApiException was not raised as expected.")
except Exception as err: # 捕获所有异常
self.assertTrue(isinstance(err, ApiException), f"Expected ApiException, but got {type(err).__name__}")
self.assertEqual(err.http_code, 403)
self.assertIn("Forbidden", err.message)
# 运行测试
# if __name__ == '__main__':
# unittest.main()注意事项:
如果您使用pytest作为测试框架,pytest.raises是一个极其强大且优雅的工具,用于测试异常。它作为一个上下文管理器,可以捕获代码块中抛出的任何异常,并允许您验证异常的类型、消息甚至更详细的属性。
import pytest
# 假设ApiException和call_gitlab_api已定义在可导入的模块中
def test_api_call_raises_api_exception_with_pytest_raises():
"""
使用pytest.raises测试当API响应不成功时,是否抛出ApiException。
"""
mock_response = MockResponse(ok=False, status_code=500, text="Internal Server Error")
with pytest.raises(ApiException) as excinfo:
call_gitlab_api(mock_response)
# excinfo对象包含了捕获到的异常信息
exception = excinfo.value # 获取实际的异常实例
assert exception.http_code == 500
assert "Internal Server Error" in exception.message
assert "ApiException" in str(exception.__class__) # 验证类名
# 可以进一步验证异常的字符串表示
assert "Error code 500 with message 'Internal Server Error'" in str(exception)
def test_api_call_raises_api_exception_with_message_check():
"""
使用pytest.raises并直接检查异常消息。
"""
mock_response = MockResponse(ok=False, status_code=400, text="Bad Request")
# 可以直接在pytest.raises中检查异常类型和部分匹配的消息
with pytest.raises(ApiException, match="Bad Request") as excinfo:
call_gitlab_api(mock_response)
assert excinfo.value.http_code == 400
def test_api_call_succeeds_with_pytest():
"""
测试当API响应成功时,不抛出异常并返回正确结果(pytest风格)。
"""
mock_response = MockResponse(ok=True, status_code=200, text='{"status": "success"}')
result = call_gitlab_api(mock_response)
assert result == {"status": "success"}优点:
在Python中测试自定义异常是确保代码健壮性的重要环节。面对isinstance()可能带来的困惑,以下是总结的几种最佳实践:
通过遵循这些策略,您可以构建出既可靠又易于维护的异常处理单元测试。
以上就是Python自定义异常的单元测试策略与常见陷阱的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号