90 votes

Poursuivre dans l'unittest de Python lorsqu'une assertion échoue

EDIT : changement pour un meilleur exemple, et clarification de la raison pour laquelle c'est un vrai problème.

J'aimerais écrire des tests unitaires en Python qui continuent à s'exécuter lorsqu'une assertion échoue, afin de pouvoir voir plusieurs échecs dans un seul test. Par exemple :

class Car(object):
  def __init__(self, make, model):
    self.make = make
    self.model = make  # Copy and paste error: should be model.
    self.has_seats = True
    self.wheel_count = 3  # Typo: should be 4.

class CarTest(unittest.TestCase):
  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    self.assertEqual(car.make, make)
    self.assertEqual(car.model, model)  # Failure!
    self.assertTrue(car.has_seats)
    self.assertEqual(car.wheel_count, 4)  # Failure!

Ici, l'objectif du test est de s'assurer que Car's __init__ définit ses champs correctement. Je pourrais le décomposer en quatre méthodes (et c'est souvent une bonne idée), mais dans ce cas, je pense qu'il est plus lisible de le garder comme une seule méthode qui teste un seul concept ("l'objet est initialisé correctement").

Si nous supposons qu'il est préférable ici de ne pas fragmenter la méthode, j'ai alors un nouveau problème : je ne peux pas voir toutes les erreurs en même temps. Lorsque je corrige la model et ré-exécuter le test, alors l'option wheel_count erreur apparaît. Cela me ferait gagner du temps de voir les deux erreurs lorsque je lance le test pour la première fois.

À titre de comparaison, le cadre de test unitaire C++ de Google fait la distinction entre entre les cas non mortels EXPECT_* assertions et fatalité ASSERT_* les assertions :

Les assertions sont présentées par paires et testent la même chose mais ont des effets différents sur la fonction courante. Les versions ASSERT_* génèrent des échecs fatals lorsqu'elles échouent, et interrompent la fonction courante. Les versions EXPECT_* génèrent des échecs non fatals, qui n'interrompent pas la fonction courante. En général, les versions EXPECT_* sont préférées, car elles permettent de signaler plusieurs échecs dans un test. Cependant, vous devriez utiliser ASSERT_* si cela n'a pas de sens de continuer lorsque l'assertion en question échoue.

Y a-t-il un moyen d'obtenir EXPECT_* -dans le langage Python unittest ? Si ce n'est pas le cas unittest Dans ce cas, existe-t-il un autre cadre de test unitaire Python qui supporte ce comportement ?


Par ailleurs, j'étais curieux de savoir combien de tests réels pouvaient bénéficier d'assertions non fatales. exemples de code (édité le 2014-08-19 pour utiliser searchcode au lieu de Google Code Search, RIP). Sur 10 résultats choisis au hasard sur la première page, tous contenaient des tests qui faisaient plusieurs assertions indépendantes dans la même méthode de test. Tous bénéficieraient d'assertions non fatales.

2 votes

Qu'avez-vous fini par faire ? Je suis intéressé par ce sujet (pour des raisons complètement différentes dont je serais heureux de discuter dans un endroit plus spacieux qu'un commentaire) et j'aimerais connaître votre expérience. Au fait, le lien "exemples de code" se termine par "Sadly, this service has been shut down", donc si vous avez une version cachée de ce lien, je serais intéressé de la voir aussi.

0 votes

Pour une référence future, je crois este est la recherche équivalente sur le système actuel, mais les résultats ne sont plus ceux décrits ci-dessus.

2 votes

@Davide, je n'ai pas fini de faire quelque chose. L'approche "une seule assertion par méthode" me semble trop rigide et dogmatique, mais la seule solution viable (et maintenable) semble être la suggestion "catch and append" d'Anthony. C'est trop laid pour moi, cependant, donc je me suis contenté de plusieurs assertions par méthode, et je devrai vivre avec des tests exécutés plus de fois que nécessaire pour trouver toutes les défaillances.

48voto

Anthony Batchelor Points 306

Une autre façon d'avoir des assertions non fatales est de capturer l'exception d'assertion et de stocker les exceptions dans une liste. Puis d'affirmer que cette liste est vide dans le cadre du tearDown.

import unittest

class Car(object):
  def __init__(self, make, model):
    self.make = make
    self.model = make  # Copy and paste error: should be model.
    self.has_seats = True
    self.wheel_count = 3  # Typo: should be 4.

class CarTest(unittest.TestCase):
  def setUp(self):
    self.verificationErrors = []

  def tearDown(self):
    self.assertEqual([], self.verificationErrors)

  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    try: self.assertEqual(car.make, make)
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertEqual(car.model, model)  # Failure!
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertTrue(car.has_seats)
    except AssertionError, e: self.verificationErrors.append(str(e))
    try: self.assertEqual(car.wheel_count, 4)  # Failure!
    except AssertionError, e: self.verificationErrors.append(str(e))

if __name__ == "__main__":
    unittest.main()

2 votes

Je suis plutôt d'accord avec vous. C'est comme ça que Selenium traite les erreurs de vérification dans le backend python.

0 votes

Oui, le problème avec cette solution est que toutes les assertions sont comptées comme des erreurs (pas des échecs) et la façon de rendre les erreurs n'est pas vraiment utilisable. Quoi qu'il en soit, il existe une solution et la fonction de rendu peut être améliorée facilement.

0 votes

J'utilise cette solution en combinaison avec La réponse de dietbudda en remplaçant toutes les assertions dans unittest.TestCase avec des blocs try / except.

34voto

hwiechers Points 4717

Une option est d'asserter sur toutes les valeurs à la fois comme un tuple.

Par exemple :

class CarTest(unittest.TestCase):
  def test_init(self):
    make = "Ford"
    model = "Model T"
    car = Car(make=make, model=model)
    self.assertEqual(
            (car.make, car.model, car.has_seats, car.wheel_count),
            (make, model, True, 4))

Le résultat de ces tests serait :

======================================================================
FAIL: test_init (test.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\temp\py_mult_assert\test.py", line 17, in test_init
    (make, model, True, 4))
AssertionError: Tuples differ: ('Ford', 'Ford', True, 3) != ('Ford', 'Model T', True, 4)

First differing element 1:
Ford
Model T

- ('Ford', 'Ford', True, 3)
?           ^ -          ^

+ ('Ford', 'Model T', True, 4)
?           ^  ++++         ^

Cela montre que le modèle et le nombre de roues sont tous deux incorrects.

0 votes

C'est intelligent. La meilleure solution que j'ai trouvée jusqu'à présent.

9voto

dietbuddha Points 4031

Ce que vous voudrez probablement faire c'est dériver unittest.TestCase puisque c'est la classe qui est lancée lorsqu'une assertion échoue. Vous devrez ré-architecturer votre TestCase à ne pas lancer (peut-être garder une liste d'échecs à la place). La ré-architecture peut causer d'autres problèmes que vous devrez résoudre. Par exemple, vous pouvez finir par avoir besoin de dériver TestSuite pour effectuer des changements en support des modifications apportées à votre TestCase .

1 votes

Je me suis dit que ce serait probablement la réponse finale, mais je voulais couvrir mes bases et voir si je ne manquais rien. Merci.

4 votes

Je dirais que c'est une surenchère de passer outre TestCase dans le but d'implémenter des assertions souples - elles sont particulièrement faciles à réaliser en python : il suffit d'attraper toutes vos AssertionError (peut-être dans une simple boucle), et les stocker dans une liste ou un ensemble, puis les faire échouer tous en même temps. Consultez la réponse de @Anthony Batchelor pour plus de détails.

2 votes

@dscordas Tout dépend si c'est pour un test unique ou si vous voulez avoir cette capacité pour la plupart des tests.

6voto

Steven Points 56939

Il est considéré comme un anti-modèle d'avoir plusieurs assertions dans un seul test unitaire. Un test unitaire unique est censé tester une seule chose. Peut-être testez-vous trop de choses. Envisagez de diviser ce test en plusieurs tests. De cette façon, vous pourrez nommer chaque test correctement.

Parfois, cependant, il est bon de vérifier plusieurs choses en même temps. Par exemple, lorsque vous affirmez les propriétés d'un même objet. Dans ce cas, vous vérifiez en fait si cet objet est correct. Une façon de procéder est d'écrire une méthode d'aide personnalisée qui sait comment affirmer sur cet objet. Vous pouvez écrire cette méthode de manière à montrer toutes les propriétés qui échouent ou, par exemple, à montrer l'état complet de l'objet attendu et l'état complet de l'objet réel lorsqu'une assertion échoue.

0 votes

Je suis d'accord pour dire que c'est bien de faire des tests aussi fins que possible, mais pas plus fins :) Je cherche de meilleures solutions pour la deuxième situation que vous mentionnez, où vous voulez vraiment vérifier plusieurs choses à la fois (comme dans le nouvel exemple que j'ai ajouté). Je pourrais écrire une aide personnalisée, mais la façon naturelle de le faire semble être d'utiliser directement les méthodes assert* de unittest.TestCase ! (sauf que je ne peux pas puisqu'elles sont fatales)

1 votes

@Bruce : Un assert doit échouer ou réussir. Jamais quelque chose entre les deux. Les tests doivent être fiables, lisibles et maintenables. Un assert qui échoue et qui n'échoue pas le test est une mauvaise idée. Cela rend vos tests trop compliqués (ce qui diminue la lisibilité et la maintenabilité) et avoir des tests qui sont "autorisés à échouer" rend facile de les ignorer, ce qui signifie qu'ils ne sont pas dignes de confiance.

8 votes

Une raison pour laquelle le reste du test ne peut pas s'exécuter et être fatal. Je pense que vous pourriez retarder le retour de l'échec quelque part en faveur de l'agrégation de tous les échecs possibles qui peuvent se produire.

5voto

Lennart Regebro Points 52510

Faites chaque assertion dans une méthode séparée.

class MathTest(unittest.TestCase):
  def test_addition1(self):
    self.assertEqual(1 + 0, 1)

  def test_addition2(self):
    self.assertEqual(1 + 1, 3)

  def test_addition3(self):
    self.assertEqual(1 + (-1), 0)

  def test_addition4(self):
    self.assertEqaul(-1 + (-1), -1)

6 votes

Je réalise que c'est une solution possible, mais ce n'est pas toujours pratique. Je cherche quelque chose qui fonctionne sans diviser un test autrefois cohésif en plusieurs petites méthodes.

0 votes

@Bruce Christensen : Si elles sont si cohérentes, alors peut-être qu'elles forment une histoire ? Et puis ils peuvent être transformés en doctrines, ce qui en effet sera continuent même après un échec.

1 votes

J'ai un ensemble de tests, quelque chose comme ceci : 1. charger les données, 2. affirmer que les données sont chargées correctement, 3. modifier les données, 4. affirmer que la modification a fonctionné correctement, 5. sauvegarder les données modifiées, 6. affirmer que les données sont sauvegardées correctement. Comment puis-je faire cela avec cette méthode ? Cela n'a pas de sens de charger les données en setup() parce que c'est l'un des tests. Mais si je place chaque assertion dans sa propre fonction, je dois alors charger les données 3 fois, ce qui constitue un énorme gaspillage de ressources. Quelle est la meilleure façon de gérer une telle situation ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X