【问题标题】:Can't catch mocked exception because it doesn't inherit BaseException无法捕获模拟异常,因为它不继承 BaseException
【发布时间】:2015-07-29 23:49:32
【问题描述】:

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

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 打包版本——据我所知,它是一个普通版本(即它们不会自己推出),但我对此并不肯定。

【问题讨论】:

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


【解决方案1】:

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

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 子句会尝试捕捉模拟。由于 mock 不是真正的 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 方法,将异常作为副作用。

【讨论】:

  • 您还可以为您的模拟设置正确的例外:from requests.exceptions import COnnectionError; mocked_requests.exceptions.ConnectionError = ConnectionError
【解决方案2】:

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

Serge 说的是对的:

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

我能想象的唯一解决方案不是模拟完整的请求,而是模拟不是异常的部分。我必须承认我找不到怎么说来模拟 模拟除此之外的所有内容

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

@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

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

【讨论】:

    【解决方案3】:

    对于我们这些需要模拟异常但不能通过简单地修补 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 替换目标,但这在这里不起作用。模拟很漂亮,但它们的行为不像异常。与其用一个 mock 打补丁,不如给它一个存根异常。我们将在我们的测试文件中这样做。

    # 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 参数调用我们给它的任何东西,并用该调用的返回替换目标。如果我们直接传递 StubException 类,它将调用该类的构造函数并用异常 instance 修补我们的目标对象,而不是我们不想要的 class .通过用lambda 包装它,它会按照我们的意图返回我们的类。

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

    【讨论】:

    • 出于好奇,您为什么需要模拟异常?
    • @Dannnno:好问题。我们模拟异常的原因与我们模拟任何东西的原因相同——以防止其逻辑在我们的单元测试中运行。通常,异常只是继承基异常类而不做任何其他事情。在这种情况下,模拟它们是没有意义的,因为我们提取的异常和我们替换它的异常在逻辑上是相同的,但时不时有人构建一个确实有一些逻辑的异常,在这种极少数情况下,很高兴能够嘲笑它。
    • 我很难想象这样一种情况,他应该做任何值得嘲笑的逻辑。如果可能的话,您能否详细说明正在发生的事情?我实际上正在为这个问题开发一个补丁,如果我能够给出一个合理的理由,那将有很大帮助。
    • 最终,你的想象力是对的。基于错误的异常应该具有值得存根的逻辑确实没有充分的理由——应该在 imo 的 try 块中处理。但有时你会使用不了解关注点分离原则的其他人的代码。如果异常不值得模拟/存根,那么我会将异常导入到我的测试文件中,使用模拟的side_effect 引发它,然后让单元捕获异常或使用self.assertRaises 断言它未被捕获。
    • 我认为值得一提的是,raise 的主要功能是使用 Exception 类来退出调用堆栈——这并不一定意味着发生了错误.一些库(通常是 Web 框架)利用这种行为在比 ifwhile 更高的级别上控制程序的流程。在这些情况下,单元测试中通常有一些值得避免的逻辑
    【解决方案4】:

    我在尝试模拟 sh 包时遇到了类似的问题。虽然 sh 非常有用,但所有方法和异常都是动态定义的,这使得模拟它们变得更加困难。所以按照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):
            ...
    

    【讨论】:

      【解决方案5】:

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

      我得到错误:

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

      当试图捕捉从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 采用的方法,但它肯定更简单,因为我不需要向我的测试添加导入并且仍然获得相同的行为。对我来说,这似乎是这里答案中推理的一般线索的合乎逻辑的结论。

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 2021-11-14
          • 2015-02-01
          • 1970-01-01
          • 2019-04-08
          • 2015-03-24
          • 1970-01-01
          • 2010-10-29
          • 1970-01-01
          相关资源
          最近更新 更多