web-dev-qa-db-ja.com

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

関数を直接実行すると、期待どおりにすべてが起こります。関数のtry節に_raise requests.exceptions.ConnectionError_を追加してテストしました。しかし、ユニットテストを実行すると、

_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アドインであるため、Python 3.3のFusion 360のパッケージバージョンを使用しています-私が知っている限り、バニラバージョン(つまり、独自のロールはしない)ですが、肯定的ではありません。

25
Dannnno

最小限の例でエラーを再現できました。

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

OK、すべて問題ありません、両方のテストに合格します

問題はモックにあります。クラス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 config。しかし、問題は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メソッドにのみパッチを適用します。

28
Serge Ballesta

sqlite3をモックしようとしたときに同じ問題に出くわしました(ソリューションを探しているときにこの投稿を見つけました)。

Serge 言ったことは正しい:

TL/DR:完全な要求パッケージをモックすると、except requests.exceptions.ConnectionError句がモックをキャッチしようとします。モックは実際には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

これが「正しい」方法であるかどうかはわかりませんが、これまでのところ、問題なく機能しているようです。

4
Bill B

例外をモックする必要があり、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クラスをそのまま渡すと、クラスのコンストラクターが呼び出され、ターゲットオブジェクトにパッチが適用されます。例外はclassではなくinstanceです。私たちが欲しいもの。 lambdaでラップすることにより、意図したとおりにクラスを返します。

パッチが完了したら、stub_exceptionオブジェクト(文字通りStubExceptionクラス)は、CustomErrorであるかのように発生および捕捉できます。きちんとした!

1
user2859458

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):
        ...
0
Wtower

struct をあざけるときに同じ問題に出くわしました。

エラーが表示されます:

TypeError:BaseExceptionを継承しないクラスをキャッチすることは許可されていません

struct.errorから発生したstruct.unpackをキャッチしようとしたとき。

私のテストでこれを回避する最も簡単な方法は、モックのエラー属性の値を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のアプローチに似ていますが、テストにインポートを追加しなくても同じ動作をする必要がないので、確かに簡単です。私には、これがここの答えの推論の一般的なスレッドへの論理的な結論であるように思えます。

0
Grr