Ne peut pas attraper moqué exception parce qu'il n'hérite pas BaseException

je travaille sur un projet qui implique de se connecter à un serveur distant, d'attendre une réponse, puis d'effectuer des actions basées sur cette réponse. Nous attrapons quelques exceptions différentes, et nous nous comportons différemment selon l'exception qui est prise. Par exemple:

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

pour tester ceci, nous avons écrit un test comme le suivant

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
            )

si j'exécute la fonction directement, tout se passe comme prévu. J'ai même testé en ajoutant raise requests.exceptions.ConnectionErrortry la clause de la fonction. Mais quand je fais les tests de mon unité, j'obtiens

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

j'ai essayé de changer l'exception que j'ai été l'application des correctifs dans BaseException et j'ai eu une erreur plus ou moins identique.

j'ai lu https://stackoverflow.com/a/18163759/3076272 déjà, donc je pense que ça doit être un mauvais __del__ crochet de quelque part, mais je ne sais pas où chercher pour elle ou ce que je peux même le faire dans le temps de le dire. Je suis relativement nouveau à unittest.mock.patch() il est donc très possible que je fasse quelque chose de mal.

C'est un add-in Fusion360 donc il utilise la version packagée de Python 3.3 de Fusion 360 - autant que je sache c'est une version vanille (i.e. ils ne roulent pas leur propre) mais je ne suis pas sûr de ça.

21
demandé sur Community 2015-07-30 02:49:32

5 réponses

je pourrais reproduire l'erreur avec un exemple minimal:

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 sans moqueur :

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, tout est très bien, à la fois le test pass

Le problème vient des simulacres. Dès que la classe MyError est moqué, le expect l'article ne peut pas attraper quoi que ce soit et je reçois la même erreur que l'exemple de la 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())

donne :

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

Ici, je reçois un premier TypeErrorque vous n'avez pas eu, parce que j'élève une moquerie alors que vous avez forcé une vraie exception avec 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError dans config. Mais le problème reste que except l'article essaie de prendre un simulacre.

TL/DR: que vous vous moquez du requests package except requests.exceptions.ConnectionError clause tente d'attraper une moquerie. Comme le simulacre n'est pas vraiment un BaseException, il provoque l'erreur.

la seule solution que je puisse imaginer est de ne pas se moquer de la totalité requests mais seulement les parties qui ne sont pas exception. Je dois admettre que je n'ai pas pu trouver comment dire de me moquer!--34 -- > moquez tout sauf ceci mais dans votre exemple, vous n'avez besoin que de patch requests.head. Je pense donc que cela devrait fonctionner:

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
        )

C'est : seul patch head méthode à l'exception comme effet secondaire.

22
répondu Serge Ballesta 2015-08-07 09:13:28

je viens de tomber sur le même problème lors de la tentative de se moquer sqlite3 (et j'ai trouvé ce post en cherchant des solutions).

Serge dit est correct:

TL / DR: lorsque vous vous moquez du paquet full requests, the except requests.exception.La clause ConnectionError essaie d'attraper une simulation. Comme la simulation n'est pas vraiment une exception de base, elle provoque l'erreur.

la seule solution que je puisse imaginer n'est pas de se moquer de toutes les requêtes mais seulement les pièces qui ne sont pas des exceptions. Je dois admettre que je n'ai pas pu trouver comment dire de me moquer!--16 -- > moquez tout sauf ceci

ma solution a été De moquer le module entier, puis de définir l'attribut mock pour l'exception à être égal à l'exception dans la classe réelle, effectivement "se moquer" de l'exception. Par exemple, dans mon cas:

@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, vous pouvez assigner des exceptions individuellement comme ceci:

    mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError

ou de le faire pour tous de l' requests exceptions comme ceci:

    mock_requests.exceptions = requests.exceptions

je ne sais pas si c'est la "bonne" façon de faire, mais jusqu'à présent, il semble fonctionner pour moi sans aucun problème.

3
répondu Bill B 2016-03-25 20:15:41

Pour ceux d'entre nous qui ont besoin de se moquer d'une exception et ne peut pas faire simplement patcher head, voici une solution facile qui remplace la cible d'exception avec un vide:

supposons que nous ayons une unité générique à tester, à une exception près, nous devons nous être moqués:

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

Nous voulons fantaisie CustomError mais comme il s'agit d'une exception, nous rencontrons des problèmes si nous essayons de la corriger comme tout le reste. Normalement, un appel à patch remplace la cible avec un MagicMock mais qui ne fonctionnent pas ici. Les moqueurs sont astucieux, mais ils ne se comportent pas comme les exceptions le font. Plutôt que de réparer avec une moquerie, donnons lui une exception de talon à la place. On le fera dans notre dossier test.

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

alors qu'est-ce qu'il y a avec le