Question

EDIT:. Commuté à un meilleur exemple, et précisé pourquoi il en est un vrai problème

Je voudrais écrire des tests unitaires en Python qui continuent d'exécution lorsqu'une assertion échoue, pour 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 faire en sorte que la __init__ de voiture définit correctement ses champs. Je pouvais le casser en quatre méthodes (et qui est souvent une bonne idée), mais dans ce cas je pense qu'il est plus facile à lire pour le garder comme une seule méthode qui teste un concept unique ( « l'objet est initialisé correctement »).

Si nous supposons qu'il est préférable ici pour ne pas briser la méthode, j'ai un nouveau problème: je ne vois pas toutes les erreurs à la fois. Quand je corriger l'erreur model et re-exécuter le test, l'affiche d'erreur wheel_count. Il me sauverait le temps de voir les erreurs quand je lance le test.

A titre de comparaison, Google C ++ framework de test unitaire établit une distinction entre entre assertions de EXPECT_* non mortels et assertions de ASSERT_* mortelles:

  

Les affirmations viennent en paires ce test de la même chose, mais avoir des effets différents sur la fonction en cours. * ASSERT_ versions génèrent des défaillances fatales quand ils échouent, et interrompra la fonction en cours. * EXPECT_ versions génèrent des défaillances, qui ne nonfatals pas abandonner la fonction actuelle. Habituellement EXPECT_ * sont préférés, car ils permettent plus d'un échec à signaler lors d'un test. Cependant, vous devez utiliser ASSERT_ * si elle n'a pas de sens de continuer en cas d'échec l'affirmation en question.

Y at-il un moyen d'obtenir un comportement semblable à EXPECT_* dans unittest Python? Dans le cas contraire à unittest, puis est-il un autre cadre de test unitaire Python qui prend en charge ce comportement?


Par ailleurs, j'étais curieux de savoir combien de tests de vie réelle pourrait bénéficier d'affirmations non mortelles, alors je regardais certains exemples de code (édité 19/08/2014 à l'utilisation searchcode au lieu de Google Recherche de code, RIP). Sur 10 résultats choisis au hasard de la première page, tous les contenus des tests qui ont fait plusieurs affirmations indépendantes dans la même méthode d'essai. Tous seraient des affirmations non mortelles.

Était-ce utile?

La solution

Qu'est-ce que vous voudrez probablement faire est unittest.TestCase Derive puisque c'est la classe qui jette lorsqu'une assertion échoue. Vous devrez re-architecte votre TestCase de ne pas jeter (garder peut-être une liste d'échecs à la place). choses-architecturer Re peut causer d'autres problèmes que vous auriez à résoudre. Par exemple, vous pouvez finir par avoir besoin de TestSuite derive d'apporter des modifications à l'appui des modifications apportées à votre TestCase.

Autres conseils

Une autre façon d'avoir des affirmations non mortelles est de capturer l'exception de l'affirmation et de stocker les exceptions dans une liste. Alors que cette liste assert 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()

L'une des options est assert 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))

La sortie de ces essais 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 incorrectes.

Il est considéré comme un anti-modèle pour avoir plusieurs en un seul affirme test unitaire. Un seul test unitaire devrait tester qu'une seule chose. Peut-être que vous testez trop. Pensez à diviser cette place de test dans 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 propriétés du même objet. Dans ce cas, vous êtes en fait d'affirmer si cet objet est correct. Une façon de le faire est d'écrire une méthode d'aide personnalisée qui sait comment faire valoir sur cet objet. Vous pouvez écrire cette méthode de manière de telle manière qu'il montre toutes les propriétés à défaut ou par exemple montre l'état complet de l'objet attendu et l'état complet de l'objet réel quand une assertion échoue.

Faites chaque assertion dans une méthode distincte.

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)

J'ai aimé l'approche par @ Anthony-Batchelor, pour capturer l'exception de AssertionError. Mais une légère variation de cette approche à l'aide des décorateurs et aussi une façon de signaler les cas de tests avec réussite / é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 __name__ == '__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

attendre est très utile dans gtest. C'est une façon python dans essentiel , et le 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

Il y a un paquet d'affirmation douce dans PyPI appelé softest qui traitera vos besoins. Il fonctionne en recueillant des échecs, en combinant les données d'exception et trace de pile, et les rapports tout dans le cadre de la production habituelle de unittest.

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 __name__ == '__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 softest

Je ne pense pas qu'il y ait un moyen de le faire avec PyUnit et ne voudrait pas voir PyUnit étendue de cette façon.

Je préfère tenir à une affirmation par fonction de test ( ou plus affirmant spécifiquement un seul concept par test ) et réécrivaient test_addition() en quatre fonctions de test séparées. Cela donnerait des informations plus utiles en cas d'échec, à savoir :

.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 appartient pas, vous pouvez trouver cette réponse utile.

Mise à jour

Il semble que vous testez deux concepts avec votre question mise à jour et je diviserait en deux ces tests unitaires. La première étant que les paramètres sont stockés sur la création d'un nouvel objet. Cela aurait deux affirmations, l'une pour make et un pour model. Si le premier échoue, le qui doit clairement être fixé, si le second passe ou n'est pas pertinent à ce stade.

Le second concept est plus discutable ... Vous testez si certaines valeurs par défaut sont initialisés. Pourquoi ? Il serait plus utile de tester ces valeurs au point qu'ils sont effectivement utilisés (et si elles ne sont pas utilisés, alors pourquoi sont-ils là?).

Ces deux tests échouent, et les deux devraient. En cas de panne, je suis unité test, je suis beaucoup plus intéressé que moi dans le succès que c'est là où 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)

J'ai eu un problème avec @Anthony Batchelor de réponse, car elle oblige-moi d'utiliser try...catch dans mes tests unitaires. Ensuite, j'encapsulé la logique de try...catch en une substitution de la méthode de TestCase.assertEqual. Ce qui suit supprimer bidouille les blocs try...catch de l'unité de code Tests:

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 __name__ == '__main__':
    unittest.main()

sortie de résultats:

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
 : 

Plus d'autres solutions pour la capture StackTrace correcte pourrait être affichée sur Comment remplacer correctement TestCase.assertEqual (), produisant le droit stacktrace?

Je me rends compte a été posé cette question il y a littéralement des années, mais il y a maintenant (au moins) deux paquets Python qui vous permettent de le faire.

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

L'autre est Python-retardée-Assertion: https://github.com/pr4bh4sh/ python-retardée assert

Je n'ai pas utilisé non plus, mais ils ont l'air assez semblable à moi.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top