从具有副作用的 class 方法引发的模拟异常给出 'did not raise'

Mock exception raised from class method with side effect gives 'did not raise'

注意:此问题基于 but modified to be different according to

使用 side_effect,我试图在调用模拟时引发 'URLError' 异常,但我收到一个我不理解的 DID NOT RAISE 错误。

我有一个 Query class 和一个 class 方法 make_request_and_get_response 可以引发几个异常。我不是main.pyget_response_from_external_api方法中捕获'URLError'异常,所以我应该能够期望被引发,随后它的异常被嘲笑。

query.py

from urllib.request import urlopen
import contextlib
import urllib

class Query:
    def __init__(self, a, b):
        self.a = a
        self.b = b

        self.query = self.make_query()


    def make_query(self):
        # create query request using self.a and self.b
        return query


    def make_request_and_get_response(self):  # <--- the 'dangerous' method that can raise exceptions
        with contextlib.closing(urlopen(self.query)) as response:
            return response.read().decode('utf-8')

main.py

from foo.query import *

def get_response_from_external_api(query):
    try:
        response = query.make_request_and_get_response()
    except urllib.error.HTTPError as e:
        print('Got a HTTPError: ', e)
    except Exception:
        print('Got a generic Exception!')
        # handle this exception


if __name__ == "__main__":    
    query = Query('input A', 'input B')
    result = get_response_from_external_api(query)
    return result

使用 pytest,我试图模拟 'dangerous' 方法 (make_request_and_get_response) 对特定异常有副作用。然后,我继续创建一个模拟的 Query 对象以在调用 make_request_and_get_response 时使用,期望最后一次调用给出 'URLError' 异常。

test_main.py

import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    @patch('foo.query.Query')
    def test_url_error(self, mockedQuery):
        with patch('foo.query.Query.make_request_and_get_response', side_effect=Exception('URLError')):
            with pytest.raises(Exception) as excinfo:
                q= mockedQuery()
                foo.main.get_response_from_external_api(q)
            assert excinfo.value = 'URLError'
            # assert excinfo.value.message == 'URLError' # this gives object has no attribute 'message'

上面的测试给出了以下错误:

>       foo.main.get_response_from_external_api(q)
E       Failed: DID NOT RAISE <class 'Exception'> id='72517784'>") == 'URLError'

即使我捕捉到 'URLError' 异常然后在 get_response_from_external_api 中重新引发它,也会发生同样的错误。

有人可以帮助我理解我缺少什么以便能够在 pytest 中引发异常吗?


根据@SimeonVisser 的回复更新

如果我修改 main.py 以删除 except Excpetion 大小写:

def get_response_from_external_api(query):
    try:
        response = query.make_request_and_get_response()
    except urllib.error.URLError as e:
        print('Got a URLError: ', e)
    except urllib.error.HTTPError as e:
        print('Got a HTTPError: ', e)

然后test_main.py中的测试:

    def test_url_error2(self):
        mock_query = Mock()
        mock_query.make_request_and_get_response.side_effect = Exception('URLError')
        with pytest.raises(Exception) as excinfo:
            get_response_from_external_api(mock_query)
        assert str(excinfo.value) == 'URLError'

测试通过 OK。

问题是 get_response_from_external_api() 已经捕获了一个 Exception 并且没有将它提升到该函数之外的任何地方。因此,当您模拟查询并使 make_request_and_get_response 引发异常时,pytest 不会看到它,因为 get_response_from_external_api() 已经捕获了它。

如果将其更改为以下内容,则 pytest 有机会看到它:

    except Exception:
        print('Got a generic Exception!')
        # handle this exception
        raise

测试用例似乎也没有按预期工作。它可以简化为以下内容(直接创建模拟查询并传递它会更容易一些):

import pytest
from unittest import mock
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    def test_url_error(self):
        mock_query = mock.Mock()
        mock_query.make_request_and_get_response.side_effect = Exception('URLError')
        with pytest.raises(Exception) as excinfo:
            get_response_from_external_api(mock_query)
        assert str(excinfo.value) == 'URLError'

然后测试用例通过(此外还用raise进行上述更改)。


对问题 1 的回答:

不同之处在于,您使用装饰器模拟 Query,然后模拟测试用例中的 class foo.query.Query 以引发该异常。但是 mockedQuery 实际上并没有改变以对该方法做任何不同的事情。所以 qmock.Mock() 的常规实例,没有任何特殊之处。

你可以改成下面这样(类似上面的做法):

import pytest
from unittest.mock import patch
from foo.query import Query
from foo.main import get_response_from_external_api


class TestExternalApiCall:
    @patch('foo.query.Query')
    def test_url_error(self, mockedQuery):
        with patch.object(mockedQuery, 'make_request_and_get_response', side_effect=Exception('URLError')):
            with pytest.raises(Exception) as excinfo:
                get_response_from_external_api(mockedQuery)
            assert str(excinfo.value) == 'URLError'

如果代码中的任何地方随后从该位置实例化一个 Query 实例,您的 with patch('foo.query.Query........ 语句将起作用。

对问题 2 的回答:

嗯,我无法重现 - 您本地的测试用例是否与问题中的相同?我不得不修改它,因为 foo.main.get_response_from_external_api(q) 不存在(foo 未被导入)所以我将其更改为调用 get_response_from_external_api(q) 并且我不断得到:

>               get_response_from_external_api(q)
E               Failed: DID NOT RAISE <class 'Exception'>