Python unittest - opposé à assertRaises?

374

Je veux écrire un test pour établir qu'une exception n'est pas déclenchée dans une circonstance donnée.

Il est simple de tester si une exception est levée ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... mais comment pouvez-vous faire le contraire .

Quelque chose comme ça dans ce que je cherche ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 
glaucon
la source
6
Vous pouvez toujours simplement faire tout ce qui devrait fonctionner dans le test. S'il déclenche une erreur, cela apparaîtra (compté comme une erreur plutôt que comme un échec). Bien sûr, cela suppose qu'il ne génère aucune erreur, plutôt qu'un simple type d'erreur défini. À part cela, je suppose que vous devrez écrire le vôtre.
Thomas K
Il s'avère que vous pouvez en fait implémenter une assertNotRaisesméthode qui partage 90% de son code / comportement avec assertRaisesenviron 30 lignes de code. Voir ma réponse ci-dessous pour plus de détails.
tél
Je veux cela afin que je puisse comparer deux fonctions avec hypothesispour m'assurer qu'elles produisent la même sortie pour toutes sortes d'entrées, tout en ignorant les cas où l'original déclenche une exception. assume(func(a))ne fonctionne pas car la sortie peut être un tableau avec une valeur de vérité ambiguë. Je veux donc simplement appeler une fonction et obtenir Truesi elle n'échoue pas. assume(func(a) is not None)fonctionne je suppose
endolith

Réponses:

394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")
DGH
la source
32
@hiwaylon - Non, c'est la bonne solution en fait. La solution proposée par user9876 est conceptuellement erronée: si vous testez la non-élévation de say ValueError, mais ValueErrorest levée à la place, votre test doit se terminer avec une condition d'échec, pas une condition d'erreur. D'un autre côté, si en exécutant le même code vous leviez un KeyError, ce serait une erreur, pas un échec. En python - différemment de certains autres langages - Les exceptions sont couramment utilisées pour le flux de contrôle, c'est pourquoi nous avons except <ExceptionName>effectivement la syntaxe. À cet égard, la solution de user9876 est tout simplement fausse.
mac
@mac - Est-ce aussi une bonne solution? stackoverflow.com/a/4711722/6648326
MasterJoe2
Celui-ci a le malheureux effet de montrer une couverture <100% (sauf que cela n'arrivera jamais) pour les tests.
Shay
3
@Shay, OMI, vous devez toujours exclure les fichiers de test eux-mêmes des rapports de couverture (car ils s'exécutent presque toujours à 100% par définition, vous gonflez artificiellement les rapports)
Original BBQ Sauce
@ original-bbq-sauce, cela ne me laisserait-il pas ouvert à des tests involontairement ignorés. Par exemple, faute de frappe dans le nom du test (ttst_function), mauvaise configuration d'exécution dans pycharm, etc.?
Shay
67

Salut - Je veux écrire un test pour établir qu'une exception n'est pas déclenchée dans une circonstance donnée.

C'est l'hypothèse par défaut - les exceptions ne sont pas levées.

Si vous ne dites rien d'autre, cela est supposé dans chaque test.

Vous n'avez pas besoin d'écrire une affirmation pour cela.

S.Lott
la source
7
@IndradhanushGupta Eh bien, la réponse acceptée rend le test plus pythonique que celui-ci. Explicite vaut mieux qu'implicite.
0xc0de
17
Aucun autre commentateur n'a souligné pourquoi cette réponse est fausse, bien que ce soit la même raison que la réponse de user9876 est fausse: les échecs et les erreurs sont des choses différentes dans le code de test. Si votre fonction était de lancer une exception lors d' un test qui ne assert, le cadre de test traiterait que comme une erreur , plutôt qu'un échec de ne pas faire valoir.
coredumperror
@CoreDumpError Je comprends la différence entre un échec et une erreur, mais cela ne vous forcerait-il pas à entourer chaque test d'une construction try / exception? Ou recommanderiez-vous de le faire uniquement pour les tests qui déclenchent explicitement une exception sous certaines conditions (ce qui signifie essentiellement que l'exception est attendue ).
federicojasson
4
@federicojasson Vous avez très bien répondu à votre propre question dans cette deuxième phrase. Les erreurs par rapport aux échecs dans les tests peuvent être décrites succinctement comme des «plantages inattendus» par rapport à un «comportement non intentionnel», respectivement. Vous voulez que vos tests affichent une erreur lorsque votre fonction se bloque, mais pas lorsqu'une exception que vous savez qu'elle lèvera, étant donné certaines entrées, sera levée lorsque différentes entrées seront fournies.
coredumperror
52

Appelez simplement la fonction. S'il déclenche une exception, le framework de tests unitaires le signalera comme une erreur. Vous pouvez ajouter un commentaire, par exemple:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)
user9876
la source
35
Les échecs et les erreurs sont conceptuellement différents. De plus, étant donné qu'en python, les exceptions sont couramment utilisées pour le flux de contrôle, cela rendra très très difficile à comprendre en un coup d'œil (= sans explorer le code de test) si vous avez cassé votre logique ou votre code ...
mac
1
Soit votre test réussit, soit il échoue. Si cela ne passe pas, vous devrez aller le réparer. Qu'elle soit signalée comme un "échec" ou une "erreur" n'a généralement pas d'importance. Il y a une différence: avec ma réponse, vous verrez la trace de la pile afin que vous puissiez voir où PathIsNotAValidOne a été lancé; avec la réponse acceptée, vous n'aurez pas ces informations, donc le débogage sera plus difficile. (En supposant que Py2; je ne sais pas si Py3 est meilleur à ce sujet).
user9876
19
@ user9876 - Non. Les conditions de sortie du test sont 3 (réussite / échec / erreur), et non 2 comme vous semblez le croire à tort. La différence entre les erreurs et les échecs est substantielle et les traiter comme s'ils étaient les mêmes n'est qu'une mauvaise programmation. Si vous ne me croyez pas, regardez simplement comment fonctionnent les exécuteurs de test et quels arbres de décision ils implémentent pour les échecs et les erreurs. Un bon point de départ pour python est le xfaildécorateur de pytest.
mac
4
Je suppose que cela dépend de la façon dont vous utilisez les tests unitaires. La façon dont mon équipe utilise les tests unitaires, ils doivent tous réussir. (Programmation agile, avec une machine d'intégration continue qui exécute tous les tests unitaires). Je sais que le scénario de test peut signaler "réussite", "échec" ou "erreur". Mais à un niveau élevé, ce qui compte vraiment pour mon équipe est "tous les tests unitaires réussissent-ils?" (c'est-à-dire "Jenkins est-il vert?"). Donc pour mon équipe, il n'y a pas de différence pratique entre «échouer» et «erreur». Vous pouvez avoir des exigences différentes si vous utilisez vos tests unitaires d'une manière différente.
user9876
1
@ user9876 la différence entre 'échec' et 'erreur' est la différence entre "mon assertion a échoué" et "mon test ne parvient même pas à l'assertion". Pour moi, c'est une distinction utile lors de la fixation des tests, mais je suppose, comme vous le dites, pas pour tout le monde.
CS
14

Je suis l'affiche originale et j'ai accepté la réponse ci-dessus par DGH sans l'avoir d'abord utilisée dans le code.

Une fois que j'ai utilisé, j'ai réalisé qu'il avait besoin d'un petit ajustement pour faire ce que j'avais besoin de faire (pour être juste envers DGH, il / elle a dit "ou quelque chose de similaire"!).

J'ai pensé qu'il valait la peine de poster le tweak ici pour le bénéfice des autres:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

Ce que j'essayais de faire ici était de garantir que si une tentative était faite pour instancier un objet Application avec un deuxième argument d'espaces, pySourceAidExceptions.PathIsNotAValidOne serait levé.

Je crois que l'utilisation du code ci-dessus (basé fortement sur la réponse de DGH) fera cela.

glaucon
la source
2
Puisque vous clarifiez votre question et ne répondez pas, vous auriez dû la modifier (sans y répondre) .Veuillez voir ma réponse ci-dessous.
hiwaylon
13
Il semble que ce soit tout le contraire du problème d'origine. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)devrait faire le travail dans ce cas.
Antony Hatchkins
8

Vous pouvez définir assertNotRaisesen réutilisant environ 90% de l'implémentation d'origine de assertRaisesdans le unittestmodule. Avec cette approche, vous vous retrouvez avec une assertNotRaisesméthode qui, à part sa condition de défaillance inversée, se comporte de manière identique à assertRaises.

TLDR et démo en direct

Il s'avère étonnamment facile d'ajouter une assertNotRaisesméthode unittest.TestCase(il m'a fallu environ 4 fois plus de temps pour écrire cette réponse que pour le code). Voici une démonstration en direct de la assertNotRaisesméthode en action . Tout commeassertRaises , vous pouvez soit passer un appelable et des arguments à assertNotRaises, soit l'utiliser dans une withinstruction. La démo en direct comprend un cas de test qui démontre que cela assertNotRaisesfonctionne comme prévu.

Détails

L'implémentation de assertRaisesin unittestest assez compliquée, mais avec un peu de sous-classement intelligent, vous pouvez remplacer et inverser sa condition d'échec.

assertRaisesest une méthode courte qui crée simplement une instance de la unittest.case._AssertRaisesContextclasse et la renvoie (voir sa définition dans le unittest.casemodule). Vous pouvez définir votre propre _AssertNotRaisesContextclasse en sous _AssertRaisesContext-classant et en remplaçant sa __exit__méthode:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Normalement, vous définissez des classes de cas de test en les faisant hériter de TestCase. Si vous héritez plutôt d'une sous-classe MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

tous vos cas de test auront désormais la assertNotRaisesméthode à leur disposition.

tel
la source
Où est votre tracebackdans votre elsedéclaration vient?
2018
1
@NOhs Il manquait un import. Son fixe
tel
2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

pourrait être modifié si vous devez accepter des paramètres.

appeler comme

self._assertNotRaises(IndexError, array, 'sort')
znotdead
la source
1

Je l'ai trouvé utile pour monkey-patch unittestcomme suit:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

Cela clarifie l'intention lors du test de l'absence d'une exception:

self.assertMayRaise(None, does_not_raise)

Cela simplifie également les tests en boucle, ce que je me retrouve souvent à faire:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))
AndyJost
la source
Qu'est-ce qu'un patch singe?
ScottMcC
1
Voir en.wikipedia.org/wiki/Monkey_patch . Après avoir ajouté assertMayRaiseà unittest.TestSuitevous pouvez simplement prétendre qu'il fait partie de la unittestbibliothèque.
AndyJost
0

Si vous transmettez une classe Exception à assertRaises(), un gestionnaire de contexte est fourni. Cela peut améliorer la lisibilité de vos tests:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Cela vous permet de tester les cas d'erreur dans votre code.

Dans ce cas, vous testez le PathIsNotAValidOneest levé lorsque vous passez des paramètres non valides au constructeur d'application.

hiwaylon
la source
1
Non, cela échouera uniquement si l'exception n'est pas déclenchée dans le bloc du gestionnaire de contexte. Peut être facilement testé par 'with self.assertRaises (TypeError): raise TypeError', qui passe.
Matthew Trevor
@MatthewTrevor Bon appel. Si je me souviens bien, plutôt que de tester le code s'exécute correctement, c'est-à-dire ne soulève pas, je proposais de tester les cas d'erreur. J'ai modifié la réponse en conséquence. J'espère que je pourrai gagner un +1 pour sortir du rouge. :)
hiwaylon
Remarque, il s'agit également de Python 2.7 et versions
qneill
0

vous pouvez essayer comme ça. try: self.assertRaises (None, function, arg1, arg2) sauf: pass si vous ne mettez pas de code à l'intérieur du bloc try, il passera par l'exception "AssertionError: None not raised" et le cas de test sera échoué. Le cas de test sera réussi si mis à l'intérieur, essayez le bloc qui est le comportement attendu.

lalit
la source
0

Une façon simple de s'assurer que l'objet est initialisé sans aucune erreur consiste à tester l'instance de type de l'objet.

Voici un exemple :

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
anroyus
la source