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

后端 未结 6 1616
无人共我
无人共我 2020-12-09 02:35

I\'m working on a project that involves connecting to a remote server, waiting for a response, and then performing actions based on that response. We catch a couple of diff

相关标签:
6条回答
  • 2020-12-09 02:41

    For those of us who need to mock an exception and can't do that by simply patching head, here is an easy solution that replaces the target exception with an empty one:

    Say we have a generic unit to test with an exception we have to have mocked:

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

    We want to mock CustomError but because it is an exception we run into trouble if we try to patch it like everything else. Normally, a call to patch replaces the target with a MagicMock but that won't work here. Mocks are nifty, but they do not behave like exceptions do. Rather than patching with a mock, let's give it a stub exception instead. We'll do that in our test file.

    # 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!
    

    So what's with the lambda? The new_callable param calls whatever we give it and replaces the target with the return of that call. If we pass our StubException class straight, it will call the class's constructor and patch our target object with an exception instance rather than a class which isn't what we want. By wrapping it with lambda, it returns our class as we intend.

    Once our patching is done, the stub_exception object (which is literally our StubException class) can be raised and caught as if it were the CustomError. Neat!

    0 讨论(0)
  • 2020-12-09 02:41

    Use patch.object to partially mock a class.

    My use case:

    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()
    
    0 讨论(0)
  • 2020-12-09 02:50

    I just ran into the same problem when mocking struct.

    I get the error:

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

    When trying to catch a struct.error raised from struct.unpack.

    I found that the simplest way to get around this in my tests was to simply set the value of the error attribute in my mock to be Exception. For example

    The method I want to test has this basic pattern:

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

    The test has this basic pattern.

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

    This is similar to the approach taken by @BillB, but it is certainly simpler as I don't need to add imports to my tests and still get the same behavior. To me it would seem this is the logical conclusion to the general thread of reasoning in the answers here.

    0 讨论(0)
  • 2020-12-09 03:02

    I just ran into the same issue while trying to mock sqlite3 (and found this post while looking for solutions).

    What Serge said is correct:

    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

    My solution was to mock the entire module, then set the mock attribute for the exception to be equal to the exception in the real class, effectively "un-mocking" the exception. For example, in my case:

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

    For requests, you could assign exceptions individually like this:

        mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError
    

    or do it for all of the requests exceptions like this:

        mock_requests.exceptions = requests.exceptions
    

    I don't know if this is the "right" way to do it, but so far it seems to work for me without any issue.

    0 讨论(0)
  • 2020-12-09 03:03

    I could reproduce the error with a minimal example:

    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"
    

    Test without mocking :

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

    Ok, all is fine, both test pass

    The problem comes with the mocks. As soon as the class MyError is mocked, the expect clause cannot catch anything and I get same error as the example from the question :

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

    Immediately gives :

    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
    

    Here I get a first TypeErrorthat you did not have, because I am raising a mock while you forced a true exception with 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError in config. But the problem remains that the except clause tries to catch a mock.

    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 but in your example, you only need to patch requests.head. So I think that this should work :

    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
            )
    

    That is : only patch the head method with the exception as side effect.

    0 讨论(0)
  • 2020-12-09 03:04

    I faced a similar issue while trying to mock the 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):
            ...
    
    0 讨论(0)
提交回复
热议问题