web-dev-qa-db-fra.com

Impossible d'attraper l'exception fictive car elle n'hérite pas de BaseException

Je travaille sur un projet impliquant la connexion à un serveur distant, l'attente d'une réponse, puis l'exécution d'actions à partir de cette réponse. Nous capturons quelques exceptions différentes et nous nous comportons différemment selon l’exception capturée. 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 cela, nous avons écrit un test comme celui-ci

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 je lance la fonction directement, tout se passe comme prévu. J'ai même testé en ajoutant raise requests.exceptions.ConnectionError à la clause try de la fonction. Mais quand je lance mes tests unitaires, je reçois 

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 sur laquelle je corrigeais en 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 ce doit être un mauvais crochet __del__ quelque part, mais je ne sais pas où le rechercher ni ce que je peux même faire dans le temps moyen. Je suis aussi relativement nouveau en unittest.mock.patch(), il est donc fort possible que je fasse quelque chose de mal là aussi.

Ceci est un complément de Fusion360, il utilise donc la version de Python 3.3 intégrée à Fusion 360 - pour autant que je sache, il s'agit d'une version Vanilla (c'est-à-dire qu'ils ne lancent pas les leurs), mais je n'en suis pas certain.

21
Dannnno

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"

Testez sans vous moquer:

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 va bien, les deux test passent

Le problème vient avec les simulacres. Dès que la classe MyError est fausse, la clause expect ne peut rien saisir et j'obtiens 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 immédiatement:

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 une première variable TypeError que vous n'aviez pas, car je soulève une maquette alors que vous avez forcé une véritable exception avec 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError dans config. Mais le problème reste que la clause except essaie d’attraper une maquette .

TL/DR: lorsque vous simulez le package requests complet, la clause except requests.exceptions.ConnectionError tente d’attraper un simulacre. Comme la maquette n'est pas vraiment une BaseException, elle provoque l'erreur.

La seule solution que je puisse imaginer n’est pas de se moquer de la requests complète, mais seulement des parties qui ne sont pas des exceptions. Je dois admettre que je ne pouvais pas trouver comment dire de se moquer de tout se moquer de cela sauf ce, mais dans votre exemple, il vous suffit de patcher requests.head. Donc, je pense 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
        )

Autrement dit, appliquez uniquement la méthode head avec l'exception comme effet secondaire.

24
Serge Ballesta

Je viens de rencontrer le même problème en essayant de simuler sqlite3 (et j'ai trouvé ce post en cherchant des solutions).

Qu'est-ce que Serge a dit est correct: 

TL/DR: lorsque vous modifiez le package de requêtes complet, la clause except requests.exceptions.ConnectionError tente d’attraper une simulation. Comme la maquette n'est pas vraiment une exception BaseException, elle provoque l'erreur. 

La seule solution que je puisse imaginer n’est pas de se moquer des demandes complètes, mais seulement des parties qui ne sont pas des exceptions. Je dois admettre que je ne trouvais pas comment dire de se moquer de se moquer de tout sauf de ça 

Ma solution consistait à simuler le module entier, puis à définir l'attribut simulé pour que l'exception soit égale à l'exception de la classe réelle, ce qui a pour effet de "dé-moduler" 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)

Pour requests, vous pouvez assigner des exceptions individuellement comme ceci:

    mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError

ou le faire pour toutes les exceptions requests comme ceci:

    mock_requests.exceptions = requests.exceptions

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

3
Bill B

Pour ceux d'entre nous qui ont besoin de se moquer d'une exception et ne peuvent le faire en appliquant simplement un correctif à head, voici une solution simple qui remplace l'exception cible par une exception vide:

Supposons que nous ayons une unité générique à tester, à une exception prè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 nous moquer de CustomError mais, comme il s'agit d'une exception, nous rencontrons des difficultés si nous essayons de le corriger comme tout le reste. Normalement, un appel à patch remplace la cible par un MagicMock mais cela ne fonctionnera pas ici. Les simulacres sont chouettes, mais ils ne se comportent pas comme les exceptions. Plutôt que de patcher avec une maquette, donnons-lui plutôt une exception de stub. Nous ferons cela dans notre fichier de 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 qui se passe avec lambda? Le paramètre new_callable appelle tout ce que nous lui donnons et remplace la cible par le retour de cet appel. Si nous passons directement notre classe StubException, elle appelle le constructeur de la classe et corrige l'objet cible avec une exception instance plutôt qu'un classe qui n'est pas ce que nous souhaitons. En l’emballant avec lambda, il retourne notre classe comme nous le souhaitons.

Une fois que notre correctif est terminé, l'objet stub_exception (qui est littéralement notre classe StubException) peut être levé et intercepté comme s'il s'agissait de CustomError. Soigné!

1
user2859458

J'ai fait face à un problème similaire en essayant de se moquer du package sh . Bien que sh soit très utile, le fait que toutes les méthodes et exceptions soient définies de manière dynamique rend plus difficile leur imitation. En suivant les recommandations de la 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

Je viens de rencontrer le même problème en moquant struct .

Je reçois l'erreur:

TypeError: les classes interceptées qui n'héritent pas de BaseException ne sont pas autorisées

Lorsque vous essayez d'attraper un struct.error généré à partir de struct.unpack.

J'ai trouvé que le moyen le plus simple de contourner ce problème dans mes tests consistait simplement à définir la valeur de l'attribut error dans mon mock sur Exception. Par exemple

La méthode que je veux tester a ce schéma de base:

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

Le test a ce modèle de base.

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

Ceci est similaire à l’approche adoptée par @BillB, mais c’est certainement plus simple car je n’ai pas besoin d’ajouter des imports à mes tests et d’avoir le même comportement. Pour moi, il semblerait que ce soit la conclusion logique du fil conducteur général du raisonnement dans les réponses données ici.

0
Grr