Frage

EDIT:. Schaltet auf ein besseres Beispiel, und klären, warum dies ist ein echtes Problem

Ich mag Unit-Tests in Python schreiben, die Ausführung fortzusetzen, wenn eine Behauptung aus, so dass ich mehrere Fehler in einem einzigen Test sehen. Zum Beispiel:

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!

Hier ist der Zweck des Tests, um sicherzustellen, dass Autos __init__ seine Felder setzt korrekt. Ich kann es in vier Methoden brechen (und das ist oft eine gute Idee), aber in diesem Fall, dass ich denke, es ist besser lesbar ist es, als eine einzige Methode zu halten, die einen einzigen Konzepttest ( „das Objekt initialisiert ist richtig“).

Wenn wir davon ausgehen, dass es hier am besten, nicht die Methode zu brechen, dann habe ich ein neues Problem: Ich kann nicht alle Fehler auf einmal sehen kann. Als ich den model Fehler und starten Sie den Test zu beheben, dann erscheint der wheel_count Fehler. Es würde mir Zeit spart beiden Fehler zu sehen, wenn ich zum ersten Mal des Test ausführen.

Zum Vergleich: Google C ++ Unit Testing Framework unterscheidet zwischen zwischen nicht-tödlichen EXPECT_* Behauptungen und fatal ASSERT_* Behauptungen:

Die Behauptungen kommen paarweise, dass Test das Gleiche, aber haben unterschiedliche Auswirkungen auf die aktuelle Funktion. ASSERT_ * Versionen erzeugen fatale Fehler auftreten, wenn sie versagen, und Abbruch der aktuellen Funktion. EXPECT_ * Versionen erzeugen nonfatal Ausfälle, die die aktuelle Funktion nicht Abbruch tun. Normalerweise EXPECT_ * sind bevorzugt, da sie erlauben, mehr als ein Fehler in einem Test gemeldet werden. Sie sollten jedoch ASSERT_ verwenden *, wenn es nicht sinnvoll fortzusetzen, wenn die Behauptung in Frage versagt.

Gibt es eine Möglichkeit EXPECT_*-ähnliches Verhalten in Python unittest zu bekommen? Wenn nicht in unittest, dann gibt es eine andere Python Unit-Test-Framework, das dieses Verhalten jedoch zwingend nötig?


übrigens, ich war neugierig, wie viele Real-Life-Tests profitieren könnte von nicht-tödlichen Behauptungen, so dass ich auf einige sah Codebeispiele (bearbeitet 2014.08.19 Gebrauch searchcode statt Google Code Search, RIP). Von 10 zufällig ausgewählte Ergebnisse aus der ersten Seite enthalten alle Tests, die mehrere unabhängige Behauptungen in dem gleichen Testverfahren hergestellt. Alle von nicht-tödlichen Behauptungen profitieren würden.

War es hilfreich?

Lösung

Was werden Sie wahrscheinlich tun mögen, ist derive unittest.TestCase da, dass die Klasse ist, die auslöst, wenn eine Behauptung fehlschlägt. Sie werden Ihre TestCase zu re-Architekten müssen nicht werfen (vielleicht eine Liste von Fehlern halten statt). Re-Architecting Sachen können auch andere Probleme verursachen, dass Sie zu lösen haben. Zum Beispiel können Sie herzuleiten TestSuite am Ende, um Änderungen zur Unterstützung der Änderungen an Ihr TestCase gemacht zu machen.

Andere Tipps

Ein andere Möglichkeit, nicht-tödliche Behauptungen haben, ist die Behauptung Ausnahme zu erfassen und die Ausnahmen in einer Liste zu speichern. Dann behauptet, dass diese Liste als Teil des tearDown leer ist.

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()

Eine Option ist assert auf alle Werte auf einmal als Tupel.

Zum Beispiel:

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))

Die Ausgabe dieses Tests wäre:

======================================================================
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)
?           ^  ++++         ^

Dies zeigt, dass sowohl das Modell und Radzahl sind falsch.

Es gilt als ein Anti-Muster mehr haben behauptet, in einem einzigen Gerät zu testen. Eine einzelne Einheit Test wird erwartet, dass nur eine Sache zu testen. Vielleicht testen Sie zu viel. Betrachten Sie Splitting diesen Test in mehrere Tests. So können Sie jeden Test richtig benennen können.

Manchmal jedoch ist es in Ordnung, mehrere Dinge gleichzeitig zu überprüfen. Zum Beispiel, wenn Sie Eigenschaften des gleichen Objekts zu behaupten. In diesem Fall sind Sie in der Tat behaupten, ob dieses Objekt korrekt ist. Ein Weg, dies zu tun, ist eine benutzerdefinierte Hilfsmethode zu schreiben, die wissen, wie man auf diesem Objekt zu behaupten. Sie können diese Methode in einer solchen Art und Weise schreiben, dass sie alle versagt Eigenschaften oder zum Beispiel zeigt zeigt den kompletten Zustand des erwarteten Objekt und der vollständige Zustand des eigentlichen Objekts, wenn eine Assertion fehlschlägt.

Sie jede Assertion in einem separaten Verfahren.

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)

mochte ich den Ansatz von @ Anthony-Batchelor, die AssertionError Ausnahme zu erfassen. Aber eine leichte Variation dieses Ansatzes Dekorateure verwenden und auch eine Möglichkeit, die Tests Fälle mit Pass / Fail zu melden.

#!/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()

Ausgabe von der Konsole:

(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

erwarten ist sehr nützlich in Gtest. Dies ist Python Weg in Kern und 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

Es ist eine weiche Behauptung Paket in PyPI genannt softest , die Ihren Anforderungen behandelt. Es funktioniert durch die Fehler sammeln, die Kombination von Ausnahme und Stack-Trace-Daten, und sie alle als Teil der üblichen unittest Ausgabe berichten.

Zum Beispiel dieses 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()

... erzeugt diese Konsole Ausgabe:

======================================================================
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)

Hinweis : Ich erstellt und pflegen softest

Ich glaube nicht, gibt es eine Möglichkeit, dies mit PyUnit zu tun und möchte nicht PyUnit verlängert auf diese Weise sehen.

Ich bevorzuge Stick eine Behauptung pro Testfunktion ( oder mehr insbesondere behauptet ein Konzept pro Test ) und würde test_addition() als vier separate Testfunktionen umschreiben. Dies würde mehr nützliche Informationen über Versagen, nämlich :

.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)

Wenn Sie sich entscheiden, dass dieser Ansatz nicht für Sie, können Sie feststellen diese Antwort hilfreich.

Update

Es sieht aus wie Sie testen zwei Konzepte mit dem aktualisierten Frage und ich würde diese in zwei Unit-Tests aufgeteilt. Der erste ist, dass der Parameter auf die Erzeugung eines neuen Objekts gespeichert werden. Dies hätte zwei Behauptungen, ein für make und einen für model. Wenn die erste nicht funktioniert, muss die, die eindeutig festgelegt werden, ob die zweite besteht oder nicht an dieser Stelle irrelevant ist.

Das zweite Konzept ist mehr fraglich ... Sie testen, ob einige Standardwerte initialisiert werden. Warum ? Es wäre sinnvoll, diese Werte an dem Punkt, zu testen, ob sie tatsächlich verwendet werden (und wenn sie nicht benutzt werden, warum sind sie dann da?).

Diese beiden Tests fehlschlagen, und beide sollten. Wenn ich Komponententests sind, bin ich weit mehr daran interessiert, Scheitern, als ich in Erfolg bin, wie das ist, wo ich zu konzentrieren muß.

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)

habe ich ein Problem mit @Anthony Batchelor Antwort, weil es Kräfte-me try...catch in meinen Unit-Tests zu verwenden. Dann verkapselt, I die try...catch Logik in einer Überschreibung der TestCase.assertEqual Methode. Die folgenden Hack entfernen Sie die try...catch Blocks von der Einheit Tests Code:

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()

Ergebnisausgabe:

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
 : 

Weitere alternative Lösungen für die korrekte Erfassung Stacktrace können auf veröffentlicht Wie richtig TestCase.assertEqual außer Kraft zu setzen (), das Recht stacktrace produzieren?

Ich weiß, diese Frage gestellt wurde buchstäblich Jahre, aber es gibt jetzt (zumindest) zwei Python-Pakete, die Sie erlauben, dies zu tun.

Eine davon ist weichsten: https://pypi.org/project/softest/

Die andere ist Python-Verzögerte-Assert: https://github.com/pr4bh4sh/ python-verzögerten assert

Ich habe auch nicht benutzt, aber sie sehen ziemlich ähnlich wie mir.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top