BEARBEITEN: wechselte zu einem besseren Beispiel und stellte klar, warum dies ein echtes Problem ist.
Ich möchte Komponententests in Python schreiben, die weiterhin ausgeführt werden, wenn eine Zusicherung fehlschlägt, damit ich mehrere Fehler in einem einzigen Test sehen kann. 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 besteht der Zweck des Tests darin, sicherzustellen, dass das Auto __init__
seine Felder korrekt einstellt. Ich könnte es in vier Methoden aufteilen (und das ist oft eine großartige Idee), aber in diesem Fall denke ich, dass es besser lesbar ist, es als einzelne Methode beizubehalten, die ein einzelnes Konzept testet ("das Objekt ist korrekt initialisiert").
Wenn wir davon ausgehen, dass es hier am besten ist, die Methode nicht aufzubrechen, habe ich ein neues Problem: Ich kann nicht alle Fehler gleichzeitig sehen. Wenn ich den model
Fehler behebe und den Test erneut ausführe, wird der wheel_count
Fehler angezeigt. Es würde mir Zeit sparen, beide Fehler zu sehen, wenn ich den Test zum ersten Mal ausführe.
Zum Vergleich unterscheidet das C ++ - Unit-Test-Framework von Google zwischen nicht schwerwiegenden EXPECT_*
und schwerwiegenden ASSERT_*
Behauptungen:
Die Behauptungen kommen paarweise, die dasselbe testen, aber unterschiedliche Auswirkungen auf die aktuelle Funktion haben. ASSERT_ * -Versionen erzeugen schwerwiegende Fehler, wenn sie fehlschlagen, und brechen die aktuelle Funktion ab. EXPECT_ * -Versionen erzeugen nicht schwerwiegende Fehler, die die aktuelle Funktion nicht abbrechen. Normalerweise werden EXPECT_ * bevorzugt, da dadurch mehr als ein Fehler in einem Test gemeldet werden kann. Sie sollten jedoch ASSERT_ * verwenden, wenn es nicht sinnvoll ist, fortzufahren, wenn die betreffende Zusicherung fehlschlägt.
Gibt es eine Möglichkeit, EXPECT_*
bei Python ein ähnliches Verhalten zu erreichen unittest
? Wenn nicht unittest
, gibt es dann ein anderes Python-Unit-Test-Framework, das dieses Verhalten unterstützt?
Im Übrigen war ich neugierig, wie viele reale Tests von nicht schwerwiegenden Behauptungen profitieren könnten, und habe mir daher einige Codebeispiele angesehen (bearbeitet am 19.08.2014, um Suchcode anstelle von Google Code Search, RIP, zu verwenden). Von 10 zufällig ausgewählten Ergebnissen auf der ersten Seite enthielten alle Tests, die mehrere unabhängige Aussagen in derselben Testmethode machten. Alle würden von nicht tödlichen Behauptungen profitieren.
quelle
Antworten:
Was Sie wahrscheinlich tun möchten, ist abzuleiten,
unittest.TestCase
da dies die Klasse ist, die ausgelöst wird, wenn eine Behauptung fehlschlägt. Sie müssen Ihre neu entwerfen,TestCase
um nicht zu werfen (möglicherweise führen Sie stattdessen eine Liste der Fehler). Eine Neuarchitektur kann andere Probleme verursachen, die Sie lösen müssten. Beispielsweise müssen Sie möglicherweise ableitenTestSuite
, um Änderungen zur Unterstützung der an Ihnen vorgenommenen Änderungen vorzunehmenTestCase
.quelle
TestCase
um weiche Behauptungen zu implementieren - sie sind in Python besonders einfach zu erstellen: Fangen Sie einfach alle IhreAssertionError
s (möglicherweise in einer einfachen Schleife) ab und speichern Sie sie in einer Liste oder einem Satz , dann scheitern sie alle auf einmal. Einzelheiten finden Sie in der Antwort von @Anthony Batchelor.Eine andere Möglichkeit, nicht schwerwiegende Zusicherungen zu erhalten, besteht darin, die Zusicherungsausnahme zu erfassen und die Ausnahmen in einer Liste zu speichern. Stellen Sie dann sicher, dass diese Liste als Teil des TearDowns 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()
quelle
unittest.TestCase
mit Try / Except- Blöcken überschreibe .Eine Option besteht darin, alle Werte gleichzeitig als Tupel zu aktivieren.
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 dieser 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 als auch die Radanzahl falsch sind.
quelle
Es wird als Anti-Muster angesehen, mehrere Asserts in einem einzigen Unit-Test zu haben. Es wird erwartet, dass ein einzelner Einheitentest nur eines testet. Vielleicht testen Sie zu viel. Teilen Sie diesen Test in mehrere Tests auf. Auf diese Weise können Sie jeden Test richtig benennen.
Manchmal ist es jedoch in Ordnung, mehrere Dinge gleichzeitig zu überprüfen. Zum Beispiel, wenn Sie Eigenschaften desselben Objekts bestätigen. In diesem Fall behaupten Sie tatsächlich, ob dieses Objekt korrekt ist. Eine Möglichkeit, dies zu tun, besteht darin, eine benutzerdefinierte Hilfsmethode zu schreiben, die weiß, wie auf diesem Objekt behauptet wird. Sie können diese Methode so schreiben, dass sie alle fehlerhaften Eigenschaften anzeigt oder beispielsweise den vollständigen Status des erwarteten Objekts und den vollständigen Status des tatsächlichen Objekts anzeigt, wenn eine Zusicherung fehlschlägt.
quelle
Seit Python 3.4 können Sie auch Untertests verwenden :
def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) with self.subTest(msg='Car.make check'): self.assertEqual(car.make, make) with self.subTest(msg='Car.model check'): self.assertEqual(car.model, model) with self.subTest(msg='Car.has_seats check'): self.assertTrue(car.has_seats) with self.subTest(msg='Car.wheel_count check'): self.assertEqual(car.wheel_count, 4)
(
msg
Parameter können Sie leichter feststellen, welcher Test fehlgeschlagen ist.)Ausgabe:
====================================================================== FAIL: test_init (__main__.CarTest) [Car.model check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 23, in test_init self.assertEqual(car.model, model) AssertionError: 'Ford' != 'Model T' - Ford + Model T ====================================================================== FAIL: test_init (__main__.CarTest) [Car.wheel_count check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 27, in test_init self.assertEqual(car.wheel_count, 4) AssertionError: 3 != 4 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=2)
quelle
Führen Sie jede Behauptung in einer separaten Methode aus.
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)
quelle
setup()
, da dies einer der Tests ist. Aber wenn ich jede Behauptung in ihre eigene Funktion setze, muss ich dreimal Daten laden, und das ist eine enorme Verschwendung von Ressourcen. Was ist der beste Weg, um mit einer solchen Situation umzugehen?In PyPI gibt es ein Soft Assertion-Paket namens
softest
, das Ihre Anforderungen erfüllt. Es sammelt die Fehler, kombiniert Ausnahme- und Stack-Trace-Daten und meldet alles als Teil des Üblichenunittest
Ausgabe.Zum Beispiel dieser 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 Konsolenausgabe:
====================================================================== 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 habe erstellt und gepflegt
softest
.quelle
Ich mochte den Ansatz von @ Anthony-Batchelor, die AssertionError-Ausnahme zu erfassen. Aber eine leichte Abweichung von diesem Ansatz unter Verwendung von Dekorateuren und auch eine Möglichkeit, die Testfälle mit Bestanden / Nicht Bestanden 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
quelle
erwarten ist sehr nützlich in gtest. Dies ist Python-Art 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
quelle
Ich hatte ein Problem mit der Antwort von @Anthony Batchelor, weil es mich gezwungen hätte, sie
try...catch
in meinen Unit-Tests zu verwenden. Stattdessen habe ich dietry...catch
Logik in eine Überschreibung derTestCase.assertEqual
Methode gekapselt . Hier ist der 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() # /programming/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 Stapelverfolgungserfassung finden Sie unter Wie kann TestCase.assertEqual () korrekt überschrieben werden, um die richtige Stapelverfolgung zu erstellen?
quelle
Ich glaube nicht, dass es eine Möglichkeit gibt, dies mit PyUnit zu tun, und möchte nicht, dass PyUnit auf diese Weise erweitert wird.
Ich halte mich lieber an eine Behauptung pro Testfunktion ( oder genauer gesagt an ein Konzept pro Test ) und würde sie
test_addition()
als vier separate Testfunktionen umschreiben . Dies würde nützlichere Informationen zum Ausfall geben, 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 entscheiden, dass dieser Ansatz nicht für Sie geeignet ist, finden Sie diese Antwort möglicherweise hilfreich.
Aktualisieren
Es sieht so aus, als würden Sie zwei Konzepte mit Ihrer aktualisierten Frage testen, und ich würde diese in zwei Komponententests aufteilen. Das erste ist, dass die Parameter beim Erstellen eines neuen Objekts gespeichert werden. Dies hätte zwei Behauptungen, eine für
make
und eine fürmodel
. Wenn der erste fehlschlägt, ist das, was eindeutig behoben werden muss, ob der zweite erfolgreich ist oder nicht, an dieser Stelle irrelevant.Das zweite Konzept ist fragwürdiger ... Sie testen, ob einige Standardwerte initialisiert sind. Warum ? Es wäre sinnvoller, diese Werte an dem Punkt zu testen, an dem sie tatsächlich verwendet werden (und wenn sie nicht verwendet werden, warum sind sie dann dort?).
Beide Tests schlagen fehl und sollten beide. Wenn ich Unit-Tests mache, bin ich viel mehr am Scheitern interessiert als am Erfolg, da ich mich dort konzentrieren muss.
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)
quelle
Mir ist klar, dass diese Frage vor Jahren buchstäblich gestellt wurde, aber jetzt gibt es (mindestens) zwei Python-Pakete, mit denen Sie dies tun können.
Eines ist am weichsten: https://pypi.org/project/softest/
Das andere ist Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Ich habe es auch nicht benutzt, aber sie sehen mir ziemlich ähnlich.
quelle