web-dev-qa-db-fra.com

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

EDIT: changé d’exemple et expliqué pourquoi il s’agit d’un problème réel.

J'aimerais écrire des tests unitaires en Python qui continuent à s'exécuter lorsqu'une assertion échoue, afin que je puisse 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, le but du test est de s’assurer que le __init__ de la voiture définit correctement ses champs. Je pourrais le diviser 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 correctement initialisé").

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

À des fins de comparaison, le framework de tests unitaires C++ de Google fait la distinction entre entre les assertions EXPECT_* non fatales et les assertions fatales ASSERT_*:

Les assertions sont des paires qui testent la même chose mais ont des effets différents sur la fonction actuelle. Les versions d'ASSERT_ * génèrent des échecs fatals lorsqu'elles échouent et annulent la fonction en cours. Les versions EXPECT_ * génèrent des échecs non fatals, qui n'abandonnent pas la fonction en cours. En règle générale, EXPECT_ * est préférable, car il permet de signaler plusieurs défaillances dans un test. Cependant, vous devez utiliser ASSERT_ * s'il n'est pas logique de continuer lorsque l'assertion en question échoue.

Existe-t-il un moyen d'obtenir un comportement semblable à EXPECT_*- dans la variable unittest de Python? Si ce n'est pas le cas dans unittest, existe-t-il un autre framework de test unitaire Python qui prend en charge ce comportement?


Incidemment, j'étais curieux de savoir combien de tests réels pourraient tirer profit d'affirmations non fatales. J'ai donc consulté quelques exemples de code (modifié le 2014-08-19 pour utiliser le code de recherche au lieu de Google Code Search, RIP). Sur 10 résultats choisis au hasard dans la première page, tous contenaient des tests ayant fait plusieurs assertions indépendantes dans la même méthode de test. Tous bénéficieraient d’affirmations non fatales.

69
Bruce Christensen

Ce que vous voudrez probablement faire, c'est dériver unittest.TestCase puisque c'est la classe qui se lève quand une assertion échoue. Vous devrez réarchiver votre TestCase pour ne pas lancer (peut-être conserver une liste des échecs à la place). La réarchitecture peut créer d’autres problèmes que vous devrez résoudre. Par exemple, vous devrez peut-être dériver TestSuite pour apporter des modifications à l'appui des modifications apportées à votre TestCase.

8
dietbuddha

Une autre façon d'avoir des assertions non fatales consiste à capturer l'exception d'assertion et à stocker les exceptions dans une liste. Ensuite, affirmez que cette liste est vide dans le cadre de 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 __== "__main__":
    unittest.main()
37
Anthony Batchelor

Une option est d'affirmer sur toutes les valeurs à la fois en tant que 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 incorrects.

26
hwiechers

Il est considéré comme un anti-modèle d'avoir plusieurs assertions dans un seul test unitaire. Un seul test unitaire ne devrait porter que sur une chose. Peut-être que vous testez trop. Envisagez de scinder ce test en plusieurs tests. De cette façon, vous pouvez nommer chaque test correctement.

Parfois, cependant, il est correct de vérifier plusieurs choses en même temps. Par exemple, lorsque vous affirmez les propriétés du même objet. Dans ce cas, vous affirmez en fait si cet objet est correct. Pour ce faire, écrivez une méthode d'assistance personnalisée qui sait comment affirmer cet objet. Vous pouvez écrire cette méthode de manière à afficher toutes les propriétés défaillantes ou, par exemple, l’état complet de l’objet attendu et l’état complet de l’objet réel lorsqu’une assertion échoue.

9
Steven

Faites chaque affirmation 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
Lennart Regebro

J'ai bien aimé l'approche de @ Anthony-Batchelor, qui consiste à capturer l'exception AssertionError. Mais une légère variation à cette approche en utilisant des décorateurs et aussi un moyen de signaler les cas de test avec succès/échec.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

class UTReporter(object):
    '''
    The UT Report class keeps track of tests cases
    that have been executed.
    '''
    def __init__(self):
        self.testcases = []
        print "init called"

    def add_testcase(self, testcase):
        self.testcases.append(testcase)

    def display_report(self):
        for tc in self.testcases:
            msg = "=============================" + "\n" + \
                "Name: " + tc['name'] + "\n" + \
                "Description: " + str(tc['description']) + "\n" + \
                "Status: " + tc['status'] + "\n"
            print msg

reporter = UTReporter()

def assert_capture(*args, **kwargs):
    '''
    The Decorator defines the override behavior.
    unit test functions decorated with this decorator, will ignore
    the Unittest AssertionError. Instead they will log the test case
    to the UTReporter.
    '''
    def assert_decorator(func):
        def inner(*args, **kwargs):
            tc = {}
            tc['name'] = func.__name__
            tc['description'] = func.__doc__
            try:
                func(*args, **kwargs)
                tc['status'] = 'pass'
            except AssertionError:
                tc['status'] = 'fail'
            reporter.add_testcase(tc)
        return inner
    return assert_decorator



class DecorateUt(unittest.TestCase):

    @assert_capture()
    def test_basic(self):
        x = 5
        self.assertEqual(x, 4)

    @assert_capture()
    def test_basic_2(self):
        x = 4
        self.assertEqual(x, 4)

def main():
    #unittest.main()
    suite = unittest.TestLoader().loadTestsFromTestCase(DecorateUt)
    unittest.TextTestRunner(verbosity=2).run(suite)

    reporter.display_report()


if __== '__main__':
    main()

Sortie de la console:

(awsenv)$ ./decorators.py 
init called
test_basic (__main__.DecorateUt) ... ok
test_basic_2 (__main__.DecorateUt) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
=============================
Name: test_basic
Description: None
Status: fail

=============================
Name: test_basic_2
Description: None
Status: pass
2
Zoro_77

Il existe dans PyPI un package d’assertions souple appelé softest qui répond à vos besoins. Cela fonctionne en collectant les échecs, en combinant les données de trace d'exception et de pile, et en les rapportant dans le cadre de la sortie unittest habituelle.

Par exemple, ce code:

import softest

class ExampleTest(softest.TestCase):
    def test_example(self):
        # be sure to pass the assert method object, not a call to it
        self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
        # self.soft_assert(self.assertEqual('Worf', 'wharf', 'Klingon is not ship receptacle')) # will not work as desired
        self.soft_assert(self.assertTrue, True)
        self.soft_assert(self.assertTrue, False)

        self.assert_all()

if __== '__main__':
    softest.main()

... produit cette sortie de la console:

======================================================================
FAIL: "test_example" (ExampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 14, in test_example
    self.assert_all()
  File "C:\...\softest\case.py", line 138, in assert_all
    self.fail(''.join(failure_output))
AssertionError: ++++ soft assert failure details follow below ++++

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The following 2 failures were found in "test_example" (ExampleTest):
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Failure 1 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 10, in test_example
    self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 829, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 1203, in assertMultiLineEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 670, in fail
    raise self.failureException(msg)
AssertionError: 'Worf' != 'wharf'
- Worf
+ wharf
 : Klingon is not ship receptacle

+--------------------------------------------------------------------+
Failure 2 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 12, in test_example
    self.soft_assert(self.assertTrue, False)
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 682, in assertTrue
    raise self.failureException(msg)
AssertionError: False is not true


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

NOTE: J'ai créé et mis à jour softest .

1
skia.heliou

expect est très utile dans gtest . Ceci est une manière python dans Gist , et code:

import sys
import unittest


class TestCase(unittest.TestCase):
    def run(self, result=None):
        if result is None:
            self.result = self.defaultTestResult()
        else:
            self.result = result

        return unittest.TestCase.run(self, result)

    def expect(self, val, msg=None):
        '''
        Like TestCase.assert_, but doesn't halt the test.
        '''
        try:
            self.assert_(val, msg)
        except:
            self.result.addFailure(self, sys.exc_info())

    def expectEqual(self, first, second, msg=None):
        try:
            self.failUnlessEqual(first, second, msg)
        except:
            self.result.addFailure(self, sys.exc_info())

    expect_equal = expectEqual

    assert_equal = unittest.TestCase.assertEqual
    assert_raises = unittest.TestCase.assertRaises


test_main = unittest.main
1
Ken

J'ai un problème avec @Anthony Batchelor answer, car cela me force à utiliser try...catch dans mes tests unitaires. Ensuite, j'ai encapsulé la logique try...catch dans un remplacement de la méthode TestCase.assertEqual. Le hack suivant supprime les blocs try...catch du code de tests unitaires:

import unittest
import traceback

class AssertionErrorData(object):

    def __init__(self, stacktrace, message):
        super(AssertionErrorData, self).__init__()
        self.stacktrace = stacktrace
        self.message = message

class MultipleAssertionFailures(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        self.verificationErrors = []
        super(MultipleAssertionFailures, self).__init__( *args, **kwargs )

    def tearDown(self):
        super(MultipleAssertionFailures, self).tearDown()

        if self.verificationErrors:
            index = 0
            errors = []

            for error in self.verificationErrors:
                index += 1
                errors.append( "%s\nAssertionError %s: %s" % ( 
                        error.stacktrace, index, error.message ) )

            self.fail( '\n\n' + "\n".join( errors ) )
            self.verificationErrors.clear()

    def assertEqual(self, goal, results, msg=None):

        try:
            super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg )

        except unittest.TestCase.failureException as error:
            goodtraces = self._goodStackTraces()
            self.verificationErrors.append( 
                    AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) )

    def _goodStackTraces(self):
        """
            Get only the relevant part of stacktrace.
        """
        stop = False
        found = False
        goodtraces = []

        # stacktrace = traceback.format_exc()
        # stacktrace = traceback.format_stack()
        stacktrace = traceback.extract_stack()

        # https://stackoverflow.com/questions/54499367/how-to-correctly-override-testcase
        for stack in stacktrace:
            filename = stack.filename

            if found and not stop and \
                    not filename.find( 'lib' ) < filename.find( 'unittest' ):
                stop = True

            if not found and filename.find( 'lib' ) < filename.find( 'unittest' ):
                found = True

            if stop and found:
                stackline = '  File "%s", line %s, in %s\n    %s' % ( 
                        stack.filename, stack.lineno, stack.name, stack.line )
                goodtraces.append( stackline )

        return goodtraces

# class DummyTestCase(unittest.TestCase):
class DummyTestCase(MultipleAssertionFailures):

    def setUp(self):
        self.maxDiff = None
        super(DummyTestCase, self).setUp()

    def tearDown(self):
        super(DummyTestCase, self).tearDown()

    def test_function_name(self):
        self.assertEqual( "var", "bar" )
        self.assertEqual( "1937", "511" )

if __== '__main__':
    unittest.main()

Résultat obtenu:

F
======================================================================
FAIL: test_function_name (__main__.DummyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\User\Downloads\test.py", line 77, in tearDown
    super(DummyTestCase, self).tearDown()
  File "D:\User\Downloads\test.py", line 29, in tearDown
    self.fail( '\n\n' + "\n\n".join( errors ) )
AssertionError: 

  File "D:\User\Downloads\test.py", line 80, in test_function_name
    self.assertEqual( "var", "bar" )
AssertionError 1: 'var' != 'bar'
- var
? ^
+ bar
? ^
 : 

  File "D:\User\Downloads\test.py", line 81, in test_function_name
    self.assertEqual( "1937", "511" )
AssertionError 2: '1937' != '511'
- 1937
+ 511
 : 

Des solutions alternatives pour la capture correcte de stacktrace pourraient être publiées sur Comment redéfinir correctement TestCase.assertEqual () pour produire le stacktrace correct?

0
user

Je sais que cette question a été posée littéralement il y a plusieurs années, mais il existe maintenant (au moins) deux packages Python permettant de le faire.

L'un est le plus doux: https://pypi.org/project/softest/

L'autre est Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert

Je n'ai pas utilisé non plus, mais ils me ressemblent beaucoup.

0
Todd Bradley

Je ne pense pas qu'il y ait un moyen de faire cela avec PyUnit et je ne voudrais pas que PyUnit soit étendu de cette manière.

Je préfère m'en tenir à une assertion par fonction de test ( ou plus spécifiquement, en affirmant un concept par test ) et réécrire test_addition() sous la forme de quatre fonctions de test distinctes. Cela donnerait plus d'informations utiles en cas d'échec, viz:

.FF.
======================================================================
FAIL: test_addition_with_two_negatives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_addition.py", line 10, in test_addition_with_two_negatives
    self.assertEqual(-1 + (-1), -1)
AssertionError: -2 != -1

======================================================================
FAIL: test_addition_with_two_positives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_addition.py", line 6, in test_addition_with_two_positives
    self.assertEqual(1 + 1, 3)  # Failure!
AssertionError: 2 != 3

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=2)

Si vous décidez que cette approche ne vous convient pas, vous pouvez trouver cette réponse utile.

Mettre à jour

Il semble que vous testiez deux concepts avec votre question mise à jour et que je les scinde en deux tests unitaires. La première est que les paramètres sont stockés lors de la création d'un nouvel objet. Cela aurait deux assertions, une pour make et une pour model. Si le premier échoue, il faut clairement régler le problème, que le second réussisse ou échoue soit sans importance à ce stade.

Le second concept est plus discutable ... Vous testez si certaines valeurs par défaut sont initialisées. Pourquoi? Il serait plus utile de tester ces valeurs au moment où elles sont réellement utilisées (et si elles ne sont pas utilisées, alors pourquoi sont-elles là?). 

Ces deux tests échouent et les deux devraient l'être. Lorsque je fais des tests unitaires, je suis beaucoup plus intéressé par l'échec que par le succès, car c'est là que je dois me concentrer.

FF
======================================================================
FAIL: test_creation_defaults (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_car.py", line 25, in test_creation_defaults
    self.assertEqual(self.car.wheel_count, 4)  # Failure!
AssertionError: 3 != 4

======================================================================
FAIL: test_creation_parameters (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_car.py", line 20, in test_creation_parameters
    self.assertEqual(self.car.model, self.model)  # Failure!
AssertionError: 'Ford' != 'Model T'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=2)
0
Johnsyweb