无法捕获模拟异常,因为它不继承 BaseException

Can't catch mocked exception because it doesn't inherit BaseException

我正在从事一个项目,该项目涉及连接到远程服务器、等待响应,然后根据该响应执行操作。我们捕获了几个不同的异常,并根据捕获到的异常采取不同的行为。例如:

def myMethod(address, timeout=20):
    try:
        response = requests.head(address, timeout=timeout)
    except requests.exceptions.Timeout:
        # do something special
    except requests.exceptions.ConnectionError:
        # do something special
    except requests.exceptions.HTTPError:
        # do something special
    else:
        if response.status_code != requests.codes.ok:
            # do something special
        return successfulConnection.SUCCESS

为了对此进行测试,我们编写了如下所示的测试

class TestMyMethod(unittest.TestCase):

    def test_good_connection(self):
        config = {
            'head.return_value': type('MockResponse', (), {'status_code': requests.codes.ok}),
            'codes.ok': requests.codes.ok
        }
        with mock.patch('path.to.my.package.requests', **config):
            self.assertEqual(
                mypackage.myMethod('some_address',
                mypackage.successfulConnection.SUCCESS
            )

    def test_bad_connection(self):
        config = {
            'head.side_effect': requests.exceptions.ConnectionError,
            'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError
        }
        with mock.patch('path.to.my.package.requests', **config):
            self.assertEqual(
                mypackage.myMethod('some_address',
                mypackage.successfulConnection.FAILURE
            )

如果我直接 运行 函数,一切都会按预期发生。我什至通过将 raise requests.exceptions.ConnectionError 添加到函数的 try 子句来进行测试。但是当我 运行 我的单元测试时,我得到

ERROR: test_bad_connection (test.test_file.TestMyMethod)
----------------------------------------------------------------
Traceback (most recent call last):
  File "path/to/sourcefile", line ###, in myMethod
    respone = requests.head(address, timeout=timeout)
  File "path/to/unittest/mock", line 846, in __call__
    return _mock_self.mock_call(*args, **kwargs)
  File "path/to/unittest/mock", line 901, in _mock_call
    raise effect
my.package.requests.exceptions.ConnectionError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "Path/to/my/test", line ##, in test_bad_connection
    mypackage.myMethod('some_address',
  File "Path/to/package", line ##, in myMethod
    except requests.exceptions.ConnectionError:
TypeError: catching classes that do not inherit from BaseException is not allowed

我试图将我正在修补的异常更改为 BaseException,但我得到了一个或多或少相同的错误。

我已经读过 ,所以我认为它一定是某个地方的 __del__ 钩子不好,但我不确定在哪里可以找到它或者我什至可以做什么与此同时。我对 unittest.mock.patch() 也比较陌生,所以很可能我在那里也做错了。

这是一个 Fusion360 加载项,因此它使用 Fusion 360 的打包版本 Python 3.3 - 据我所知这是一个原始版本(即他们不自己推出)但我'我对此并不肯定。

我可以用一个最小的例子重现错误:

foo.py:

class MyError(Exception):
    pass

class A:
    def inner(self):
        err = MyError("FOO")
        print(type(err))
        raise err
    def outer(self):
        try:
            self.inner()
        except MyError as err:
            print ("catched ", err)
        return "OK"

没有模拟的测试:

class FooTest(unittest.TestCase):
    def test_inner(self):
        a = foo.A()
        self.assertRaises(foo.MyError, a.inner)
    def test_outer(self):
        a = foo.A()
        self.assertEquals("OK", a.outer())

好的,一切正常,两个测试都通过

问题来自模拟。一旦 class MyError 被嘲笑, expect 子句就无法捕捉到任何东西,我得到与问题示例相同的错误:

class FooTest(unittest.TestCase):
    def test_inner(self):
        a = foo.A()
        self.assertRaises(foo.MyError, a.inner)
    def test_outer(self):
        with unittest.mock.patch('foo.MyError'):
            a = exc2.A()
            self.assertEquals("OK", a.outer())

立即给出:

ERROR: test_outer (__main__.FooTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "...\foo.py", line 11, in outer
    self.inner()
  File "...\foo.py", line 8, in inner
    raise err
TypeError: exceptions must derive from BaseException

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#78>", line 8, in test_outer
  File "...\foo.py", line 12, in outer
    except MyError as err:
TypeError: catching classes that do not inherit from BaseException is not allowed

这里我得到了一个你没有的第一个 TypeError,因为当你在配置中用 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError 强制一个真正的异常时我正在引发一个模拟。但问题仍然存在,except 子句试图捕获模拟

TL/DR:当您模拟完整的 requests 包时,except requests.exceptions.ConnectionError 子句会尝试捕获模拟。由于模拟不是真正的 BaseException,它会导致错误。

我能想到的唯一解决方案是不模拟完整的 requests,而只模拟不例外的部分。我必须承认,我找不到如何模拟 模拟除此 以外的所有内容,但在您的示例中,您只需要修补 requests.head。所以我认为这应该有效:

def test_bad_connection(self):
    with mock.patch('path.to.my.package.requests.head',
                    side_effect=requests.exceptions.ConnectionError):
        self.assertEqual(
            mypackage.myMethod('some_address',
            mypackage.successfulConnection.FAILURE
        )

即:仅修补 head 方法,但副作用除外。

我只是 运行 在尝试模拟 sqlite3 时遇到了同样的问题(并在寻找解决方案时发现了这个 post)。

说的是正确的:

TL/DR: as you mock the full requests package, the except requests.exceptions.ConnectionError clause tries to catch a mock. As the mock is not really a BaseException, it causes the error.

The only solution I can imagine is not to mock the full requests but only the parts that are not exceptions. I must admit I could not find how to say to mock mock everything except this

我的解决方案是模拟整个模块,然后将异常的模拟属性设置为等于真实 class 中的异常,有效地“取消模拟”异常。例如,在我的例子中:

@mock.patch(MyClass.sqlite3)
def test_connect_fail(self, mock_sqlite3):
    mock_sqlite3.connect.side_effect = sqlite3.OperationalError()
    mock_sqlite3.OperationalError = sqlite3.OperationalError
    self.assertRaises(sqlite3.OperationalError, MyClass, self.db_filename)

对于 requests,您可以像这样单独分配例外:

    mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError

或者像这样对所有 requests 异常执行此操作:

    mock_requests.exceptions = requests.exceptions

我不知道这是否是“正确”的做法,但到​​目前为止,它似乎对我没有任何问题。

对于我们这些需要模拟异常但不能通过简单地修补 head 来做到这一点的人来说,这里有一个简单的解决方案,可以用空异常替换目标异常:

假设我们有一个通用单元要测试,但我们必须模拟一个异常:

# app/foo_file.py
def test_me():
    try:
       foo()
       return "No foo error happened"
    except CustomError:  # <-- Mock me!
        return "The foo error was caught"

我们想模拟 CustomError 但因为它是一个例外,所以如果我们像其他任何东西一样尝试修补它,我们 运行 就会遇到麻烦。通常,调用 patch 会用 MagicMock 替换目标,但这在这里不起作用。模拟很漂亮,但它们的行为不像异常那样。与其用模拟打补丁,不如给它一个存根异常。我们将在我们的测试文件中这样做。

# app/test_foo_file.py
from mock import patch


# A do-nothing exception we are going to replace CustomError with
class StubException(Exception):
    pass


# Now apply it to our test
@patch('app.foo_file.foo')
@patch('app.foo_file.CustomError', new_callable=lambda: StubException)
def test_foo(stub_exception, mock_foo):
    mock_foo.side_effect = stub_exception("Stub")  # Raise our stub to be caught by CustomError
    assert test_me() == "The error was caught"

# Success!

那么 lambda 是怎么回事? new_callable 参数调用我们给它的任何内容,并用该调用的 return 替换目标。如果我们直接传递 StubException class,它将调用 class 的构造函数并使用异常 instance 而不是 a 来修补我们的目标对象class 这不是我们想要的。通过用 lambda 包装它,它 return 就是我们想要的 class。

一旦我们的修补完成,stub_exception 对象(实际上就是我们的 StubException class)就可以像 CustomError 一样被引发和捕获。整洁!

我在尝试模拟 sh package. While sh is very useful, the fact that all methods and exceptions are defined dynamically make it more difficult to mock them. So following the recommendation of the documentation 时遇到了类似的问题:

import unittest
from unittest.mock import Mock, patch


class MockSh(Mock):
    # error codes are defined dynamically in sh
    class ErrorReturnCode_32(BaseException):
        pass

    # could be any sh command    
    def mount(self, *args):
        raise self.ErrorReturnCode_32


class MyTestCase(unittest.TestCase):
    mock_sh = MockSh()

    @patch('core.mount.sh', new=mock_sh)
    def test_mount(self):
        ...

我在模拟 struct 时 运行 遇到了同样的问题。

我收到错误:

TypeError: catching classes that do not inherit from BaseException is not allowed

当试图捕捉从 struct.unpack 引发的 struct.error 时。

我发现在我的测试中解决这个问题的最简单方法是简单地将我的模拟中的错误属性的值设置为 Exception。例如

我要测试的方法有这个基本模式:

def some_meth(self):
    try:
        struct.unpack(fmt, data)
    except struct.error:
        return False
    return True

测试有这个基本模式。

@mock.patch('my_module.struct')
def test_some_meth(self, struct_mock):
    '''Explain how some_func should work.'''
    struct_mock.error = Exception
    self.my_object.some_meth()
    struct_mock.unpack.assert_called()
    struct_mock.unpack.side_effect = struct_mock.error
    self.assertFalse(self.my_object.some_meth()

这与@BillB 采取的方法类似,但它肯定更简单,因为我不需要将导入添加到我的测试中并且仍然得到相同的行为。在我看来,这似乎是此处答案中一般推理线索的合乎逻辑的结论。

使用 patch.object 部分模拟 class。

我的用例:

import unittest
from unittest import mock
import requests

def test_my_function(self):
    response = mock.MagicMock()
    response.raise_for_status.side_effect = requests.HTTPError

    with mock.patch.object(requests, 'get', return_value=response):
        my_function()