使用 'pytest.raises()' 捕获自定义异常并断言错误消息

Capture a custom exception with 'pytest.raises()' and assert the error message

我正在使用 pytest 到 运行 一些单元测试。我的一些测试涉及使用无效参数初始化对象,并测试引发的异常是否包含预期的错误消息。

为此,我使用 raises,但是 pytest 未通过测试而不是捕获异常。测试输出显示出现了预期的异常。

>>> def test_400_invalid_org_id(self):
... 
...     # Setting host, org_id and body here.
... 
...     with pytest.raises(InvalidOrgIdException) as e_info:
...         OrgRequest(host, org_id, body)
... 
...     assert str(e_info.value) == 'Invalid organisation ID.'
E           InvalidOrgIdException: Invalid organisation ID.

显然 InvalidOrgIdException 是自定义类型(正确导入),是 Exception 的子类。出于某种原因 pytest.raises(Exception) 按预期工作。查看 documentation,它表明我应该能够断言已捕获的异常类型,但这也失败了。

>>> def test_400_invalid_org_id(self):
... 
...     # Setting host, org_id and body here.
... 
...     with pytest.raises(Exception) as e_info:
...         OrgRequest(host, org_id, body)
...
...     assert str(e_info.value) == 'Invalid organisation ID.'
...     assert e_info.type is InvalidOrgIdException
E       AssertionError: assert <class 'InvalidOrgIdException'> is InvalidOrgIdException
E        +  where <class 'InvalidOrgIdException'> = <ExceptionInfo InvalidOrgIdException('xxx', '^[a-z0-9]{16}$') tblen=3>.type

比较e_info.typeInvalidOrgIdException时,__init____str__方法有区别。请注意,导入的对象都来自同一个模块 - 我也不是在嘲笑。

>>> pprint(vars(e_info.type))
mappingproxy({'__doc__': 'Organisation ID in path is invalid.',
              '__init__': <function InvalidOrgIdException.__init__ at 0x7f58b867e8b0>,
              '__module__': 'builtins',
              '__str__': <function InvalidOrgIdException.__str__ at 0x7f58b867e9d0>,
              '__weakref__': <attribute '__weakref__' of 'InvalidOrgIdException' objects>})
...
>>> pprint(vars(InvalidOrgIdException))
mappingproxy({'__doc__': 'Organisation ID in path is invalid.',
              '__init__': <function InvalidOrgIdException.__init__ at 0x7f58b867e670>,
              '__module__': 'builtins',
              '__str__': <function InvalidOrgIdException.__str__ at 0x7f58b867e700>,
              '__weakref__': <attribute '__weakref__' of 'InvalidOrgIdException' objects>})

那么为什么 pytest 会这样,是否可以改变这种行为?


Class 引发 InvalidOrgIdException

class OrgRequest():
    def __init__(self) -> None:
        raise InvalidOrgIdException() from None

来自 pytest 运行

的完整输出

正在从 myproject 目录中 运行 进行测试(请参阅问题底部的文件结构)。

python -m pytest tests/unit/test_requests.py --verbose

测试 运行 带有 --verbose 参数。我不是 pytest 专家,但我认为没有更详细的输出可用。

========================================================================================= test session starts ==========================================================================================
platform linux -- Python 3.8.10, pytest-7.0.1, pluggy-1.0.0 -- /usr/bin/python
cachedir: .pytest_cache
rootdir: /home/me/myproject/tests, configfile: pytest.ini
plugins: env-0.6.2, mock-3.7.0
collected 1 item                                                                                                                                                                                       

tests/unit/test_requests.py::TestRequests::test__400_org_id_invalid FAILED

=============================================================================================== FAILURES ===============================================================================================
_________________________________________________________________________________ TestRequests.test_400_org_id_invalid _________________________________________________________________________________

self = <tests.unit.test_requests.TestRequests object at 0x7f7627e07e20>

    def test_400_org_id_invalid(self):
        with pytest.raises(InvalidOrgIdException) as e_info:
>           OrgRequest()

tests/unit/test_requests.py:9: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <myfunction.core.request.OrgRequest object at 0x7f7627e20130>

    def __init__(self) -> None:
>       raise InvalidOrgIdException() from None
E       InvalidOrgIdException: Invalid organisation ID.

myfunction/core/request.py:19: InvalidOrgIdException
======================================================================================= short test summary info ========================================================================================
FAILED tests/unit/test_requests.py::TestRequests::test_400_org_id_invalid - InvalidOrgIdException: Invalid organisation ID.
========================================================================================== 1 failed in 0.06s ===========================================================================================

文件结构

myproject
├── myfunction
|   ├── app.py
|   └── core
|       ├── exception.py
|       └── request.py
└── tests
    └── unit
        └── test_requests.py

Hacky“修复”

如果我将 InvalidOrgIdException 移动到与 OrgRequest 相同的模块中,然后从那里导入异常,则测试通过。显然,尽管我不想这样做,因为所有异常一起生活是有意义的。尽管存在“修复”,但我仍然想了解正在发生的事情以及它起作用的确切原因。

from app import InvalidOrgIdException

正如我在问题中提到的,pytest.raises() 捕获的异常的比较似乎与从我的代码导入的异常不同。我必须承认,我不是 Python 期望的,我不明白为什么这有效,但解决方案是添加 init.py core 模块,并在那里导入该包中的所有模块。

from core.exception import *
from core.logging import *
# etc.

然后在 tests/unit/test_requests.py 中,我能够从 core 包中导入所有对象。

from myfunction.core import *

现在 pytest 看到它捕获的异常和导入的异常一样,我的测试通过了。