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.
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.
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.
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é!
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):
...
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.