in Python Unittest Fortsetzung, wenn eine Behauptung fehlschlägt
-
12-10-2019 - |
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.
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.