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

时间:2015-07-29 23:49:33

标签: python exception-handling python-requests python-3.3 python-mock

我正在处理一个涉及连接到远程服务器,等待响应,然后根据该响应执行操作的项目。我们捕获了几个不同的异常,并且根据捕获的异常而表现不同。例如:

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,但我得到了或多或少相同的错误。

我已经阅读了https://stackoverflow.com/a/18163759/3076272,所以我认为它必定是一个错误的__del__钩子,但我不确定在哪里寻找它或我甚至可以同时做。我对unittest.mock.patch()也相对较新,所以我很有可能在那里做错了。

这是一个Fusion360插件,因此它使用的是Fusion 360的Python 3.3打包版本 - 据我所知它是一个普通版本(即他们不会推出自己的版本)但我并不是肯定的。

6 个答案:

答案 0 :(得分:27)

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

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())

好的,一切都很好,两次测试通过

问题来自嘲笑。一旦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方法,副作用为异常。

答案 1 :(得分:4)

我在尝试模拟sqlite3时遇到了同样的问题(并在寻找解决方案时发现了这篇文章)。

Serge说的是正确的:

  

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

     

我能想象的唯一解决方案不是模拟完整的请求,而只模拟非例外的部分。我必须承认我无法找到如何模仿模拟除了

之外的所有内容

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

@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

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

答案 2 :(得分:1)

对于我们这些需要模拟异常并且无法通过简单地修补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 param调用我们提供的任何内容,并用该调用的返回替换目标。如果我们直接传递StubException类,它将调用类的构造函数并使用异常实例而不是修补我们的目标对象。不是我们想要的。通过用lambda包装它,它会按照我们的意图返回我们的类。

完成修补后,stub_exception对象(实际上是我们的StubException类)可以被引发并被捕获,就像它是CustomError一样。整齐!

答案 3 :(得分:0)

我在尝试模拟sh包时遇到了类似的问题。虽然sh非常有用,但动态定义所有方法和异常的事实使得模拟它们变得更加困难。因此,遵循documentation

的建议
ID

答案 4 :(得分:0)

我在模仿struct时遇到了同样的问题。

我收到错误:

  

TypeError:不允许捕获不从BaseException继承的类

尝试抓住struct.error时提出的struct.unpack

我发现在我的测试中解决这个问题的最简单方法是简单地将mock中的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采用的方法,但它当然更简单,因为我不需要在我的测试中添加导入并仍然获得相同的行为。对我来说,这似乎是这里答案中推理的一般思路的逻辑结论。

答案 5 :(得分:0)

使用patch.object来部分模拟一个类。

我的用例:

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()