Proseguendo in unittest del Python quando un'asserzione fallisce
-
12-10-2019 - |
Domanda
EDIT:. Commutata ad un esempio migliore, e chiarito il motivo per cui questo è un vero problema
Mi piacerebbe scrivere unit test in Python che continuare l'esecuzione quando un'asserzione fallisce, in modo che posso vedere più errori in un singolo test. Ad esempio:
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!
Qui, lo scopo del test è quello di garantire che __init__
di auto imposta correttamente i suoi campi. Potrei suddividerlo in quattro metodi (e questo è spesso una grande idea), ma in questo caso penso che sia più leggibile per tenerlo come un unico metodo che mette alla prova un singolo concetto ( "l'oggetto è inizializzato correttamente").
Se si assume che è meglio qui per non rompere il metodo, poi ho un nuovo problema: non riesco a vedere tutti gli errori in una sola volta. Quando posso correggere l'errore model
ed eseguire nuovamente il test, poi il compare di errore wheel_count
. Mi farebbe risparmiare tempo di vedere entrambi gli errori quando ho eseguire il test.
Per fare un confronto, di Google C ++ quadro unit testing distingue tra tra asserzioni non fatali EXPECT_*
e asserzioni ASSERT_*
fatali:
Le affermazioni venire a coppie che prova la stessa cosa, ma hanno effetti diversi sulla funzione corrente. ASSERT_ * versioni generano errori fatali quando falliscono, e interrompere la funzione corrente. EXPECT_ * versioni generano fallimenti non fatali, che non interrompere la funzione corrente. Solitamente EXPECT_ * sono preferibili, in quanto consentono più di uno guasti da segnalare in una prova. Tuttavia, è necessario utilizzare ASSERT_ * se non ha senso continuare a quando l'affermazione in questione non riesce.
C'è un modo per ottenere EXPECT_*
simile comportamento in unittest
di Python? Se non in unittest
, poi c'è un altro framework di unit test Python che supporta questo comportamento?
Per inciso, ero curioso di sapere quanti test di vita reale possono trarre vantaggio da affermazioni non fatali, così ho guardato un po ' esempi di codice (a cura 2014/08/19 per uso searchcode al posto di Google code Search, RIP). Di 10 risultati selezionati casualmente dalla prima pagina, tutti i test che hanno reso più asserzioni indipendenti nello stesso metodo di prova contenuta. Tutto sarebbe beneficiare di asserzioni non fatali.
Soluzione
Quello che probabilmente si vorrà fare è unittest.TestCase
derive dato che è la classe che lancia quando un'asserzione fallisce. Si dovrà ri-architetto vostra TestCase
di non buttare (forse tenere un elenco di fallimenti, invece). Re-architecting roba può causare altri problemi che si devono risolvere. Per esempio si può finire per dover TestSuite
Derive per apportare modifiche a sostegno delle modifiche apportate al TestCase
.
Altri suggerimenti
Un altro modo per avere asserzioni non fatali è quello di catturare l'eccezione asserzione e memorizzare le eccezioni in un elenco. Quindi affermare che tale elenco è vuota come parte del 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()
Una possibilità è assert su tutti i valori in una volta come una tupla.
Ad esempio:
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))
L'output di questo test potrebbe essere:
======================================================================
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)
? ^ ++++ ^
Questo dimostra che sia il modello e il conteggio ruota sono corretti.
E 'considerato un anti-pattern per avere più asserisce in un singolo test di unità. Una singola unit test è previsto per testare una sola cosa. Forse si sta testando troppo. Pensare di dividere questo test fino in prove multiple. In questo modo è possibile assegnare un nome ogni test in modo corretto.
A volte però, è bene controllare più cose allo stesso tempo. Per esempio quando si stanno affermando le proprietà dello stesso oggetto. In tal caso, si sono infatti affermando se tale oggetto è corretta. Un modo per farlo è quello di scrivere un metodo di supporto personalizzato che sa far valere su tale oggetto. È possibile scrivere tale metodo in tal modo un che mostra tutte le proprietà mancanza o per esempio mostra lo stato completa dell'oggetto previsto e lo stato completa dell'oggetto effettivo quando un'asserzione fallisce.
fare ogni assert in un metodo separato.
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)
Mi è piaciuto il metodo da @ Anthony-Batchelor, per catturare l'eccezione AssertionError. Ma una lieve variazione a questo approccio utilizzando decoratori e anche un modo per segnalare i casi di test con pass / fail.
#!/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()
Output da 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
aspettare è molto utile in GTEST. Questo è il modo in pitone Gist , e il codice:
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
Esiste un pacchetto asserzione morbido in PyPI chiamato softest
che consente di gestire le vostre esigenze. Funziona attraverso la raccolta di fallimenti, combinando i dati di eccezione e traccia dello stack, e reporting tutto come parte della solita uscita unittest
.
Per esempio, questo codice:
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()
... produce questo output della 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)
Nota ??strong>: ho creato e mantengo softest
Non credo ci sia un modo per fare questo con PyUnit e non vorrebbe vedere PyUnit esteso in questo modo.
preferisco attenersi a un'affermazione per ogni funzione di test ( o più specificamente affermando un concetto per test ) e sarebbe riscrivere test_addition()
come quattro funzioni di test separati. Questo darebbe informazioni più utili in caso di fallimento, e cioè :
.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)
Se si decide che questo approccio non è per voi, si possono trovare questa risposta utile.
Aggiorna
Sembra che si sta testando due concetti con la tua domanda aggiornato e vorrei dividere questi in due test di unità. Il primo è che i parametri vengono memorizzati sulla creazione di un nuovo oggetto. Ciò avrebbe due asserzioni, una per make
e uno per model
. Se la prima fallisce, il che deve chiaramente da fissare, se il secondo passaggio o non è irrilevante in questo frangente.
Il secondo concetto è più discutibile ... si sta testando se alcuni valori di default vengono inizializzati. Perché ? Sarebbe più utile per testare questi valori al punto che essi sono effettivamente utilizzati (e se non vengono utilizzati, allora perché sono lì?).
Entrambi questi test falliscono, ed entrambi dovrebbero. Quando sono unit test, io sono molto più interessato a fallimento di me nel successo come quello è dove ho bisogno di concentrarmi.
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)
Ho un problema con la @Anthony Batchelor risposta, perché costringe-me di utilizzare try...catch
all'interno mio test di unità. Poi, ho incapsulato la logica try...catch
in un override del metodo TestCase.assertEqual
. Il seguente trucco rimuovere i blocchi try...catch
dalla unit test del codice:
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()
uscita Risultato:
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
:
soluzioni più alternative per la cattura stacktrace corretta potrebbe essere pubblicato su Come ignorare correttamente TestCase.assertEqual (), che produce lo stacktrace giusto?
Mi rendo conto che questa domanda è stato chiesto letteralmente anni fa, ma ora ci sono (almeno) due pacchetti di Python che ti permettono di fare questo.
Una è più morbido: https://pypi.org/project/softest/
L'altro è Python-Delayed-Assert: https://github.com/pr4bh4sh/ python-ritardo-assert
Non ho usato sia, ma guardare piuttosto simile a me.