Scopo | Ambiente per l'automazione dei test |
Versione Python | 2.1 |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo unittest
Il modulo unittest , a cui talvolta ci si riferisce come PyUnit, è basato sul progetto dell'ambiente XUnit, di Kent Beck ed Erich Gamma. Lo stesso modello viene ripetuto in molti altri linguaggi, incluso C, perl, Java e Smalltalk. L'ambiente implementato da unittest supporta fixtures (impianti di test), test suites (raccolte di test) ed un test runner (esecutore di test) per consentire l'automazione del test per il proprio codice.
I test, così come definiti da
unittest
, sono composti da due parti: il codice per gestire "l'impianto" di test, ed il test stesso. Test individuali sono creati subclassando
TestCase
e riscrivendo od aggiungendo i metodi appropriati. Ad esempio:
import unittest
class SimplisticTest(unittest.TestCase):
def test(self):
self.failUnless(True)
if __name__ == '__main__':
unittest.main()
In questo caso, SimplisticTest ha un singolo metodo
test()
, che fallirebbe se True fosse mai False.
Il modo più semplice per eseguire i test di unittest è di includere:
if __name__ == '__main__':
unittest.main()
alla fine di ogni file di test, poi semplicemente eseguire lo script direttamente da riga di comando:
$ python unittest_simple.py . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
L'output abbreviato comprende il tempo impiegato per il test, assieme ad un indicatore di stato per ogni test (il puntino nella prima riga dell'output significa che un test è stato superato). Per maggiori dettagli nel risultato del test si include l'opzione -v :
$ python unittest_simple.py -v test (__main__.SimplisticTest) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
I test hanno 3 risultati possibili:
Non esiste un modo esplicito per "far superare" un test, quindi lo stato del test dipende dalla presenza (od assenza) di una eccezione.
import unittest
class OutcomesTest(unittest.TestCase):
def testPass(self):
return
def testFail(self):
self.failIf(True)
def testError(self):
raise RuntimeError('Errore nel test!')
if __name__ == '__main__':
unittest.main()
Quando un test fallisce o genera un errore, nell'ouput viene incluso anche il traceback.
$ python unittest_outcomes.py EF. ====================================================================== ERROR: testError (__main__.OutcomesTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_outcomes.py", line 15, in testError raise RuntimeError('Errore nel test!') RuntimeError: Errore nel test! ====================================================================== FAIL: testFail (__main__.OutcomesTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_outcomes.py", line 12, in testFail self.failIf(True) AssertionError ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1, errors=1)
Nell'esempio di cui sopra,
testFail()
fallisce, ed il traceback mostra la riga che comprende il codice che ha fallito. E' comunque compito di colui che legge il risultato del test di verificare il codice per desumere il significato semantico del fallimento del test. Per facilitare la comprensione della natura del fallimento del test, i metodi
fail*()
ed
assert*()
accettano tutti un parametro
msg
, che può essere usato per produrre un messaggio di errore più dettagliato.
import unittest
class FailureMessageTest(unittest.TestCase):
def testFail(self):
self.failIf(True, 'Il messaggio di fallimento va qui')
if __name__ == '__main__':
unittest.main()
$ python unittest_failwithmessage.py -v testFail (__main__.FailureMessageTest) ... FAIL ====================================================================== FAIL: testFail (__main__.FailureMessageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_failwithmessage.py", line 9, in testFail self.failIf(True, 'Il messaggio di fallimento va qui') AssertionError: Il messaggio di fallimento va qui ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
La maggior parte dei test affermano la verità di una qualche condizione. Ci sono alcuni diversi modi di scrivere dei test che verifichino una verità, a seconda della prospettiva dell'autore del test ed del risultato voluto del codice che si sta verificando. Se il codice produce un valore che può essere valutato come vero, dovrebbero essere usati i metodi
failUnless()
ed
assertTrue()
. Se il codice produce un valore falso, ha più senso usare i metodi
failIf()
ed
assertFalse()
.
import unittest
class TruthTest(unittest.TestCase):
def testFailUnless(self):
self.failUnless(True)
def testAssertTrue(self):
self.assertTrue(True)
def testFailIf(self):
self.failIf(False)
def testAssertFalse(self):
self.assertFalse(False)
if __name__ == '__main__':
unittest.main()
$ python unittest_failwithmessage.py -v testFail (__main__.FailureMessageTest) ... FAIL ====================================================================== FAIL: testFail (__main__.FailureMessageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_failwithmessage.py", line 9, in testFail self.failIf(True, 'Il messaggio di fallimento va qui') AssertionError: Il messaggio di fallimento va qui ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1) robby@robby-desktop:~/pydev/pymotw-it/dumpscripts$ python unittest_truth.py -v testAssertFalse (__main__.TruthTest) ... ok testAssertTrue (__main__.TruthTest) ... ok testFailIf (__main__.TruthTest) ... ok testFailUnless (__main__.TruthTest) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK
Come caso speciale, unittest comprende metodi per verificare l'eguaglianza di due valori.
import unittest
class EqualityTest(unittest.TestCase):
def testEqual(self):
self.failUnlessEqual(1, 3-2)
def testNotEqual(self):
self.failIfEqual(2, 3-2)
if __name__ == '__main__':
unittest.main()
$ python unittest_failwithmessage.py -v testFail (__main__.FailureMessageTest) ... FAIL ====================================================================== FAIL: testFail (__main__.FailureMessageTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_failwithmessage.py", line 9, in testFail self.failIf(True, 'Il messaggio di fallimento va qui') AssertionError: Il messaggio di fallimento va qui ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1) robby@robby-desktop:~/pydev/pymotw-it/dumpscripts$ python unittest_truth.py -v testAssertFalse (__main__.TruthTest) ... ok testAssertTrue (__main__.TruthTest) ... ok testFailIf (__main__.TruthTest) ... ok testFailUnless (__main__.TruthTest) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK robby@robby-desktop:~/pydev/pymotw-it/dumpscripts$ python unittest_equality.py -v testEqual (__main__.EqualityTest) ... ok testNotEqual (__main__.EqualityTest) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
Questi test speciali fanno comodo, visto che i valori confrontati appaiono nel messaggio di fallimento, quando un test fallisce.
import unittest
class InequalityTest(unittest.TestCase):
def testEqual(self):
self.failIfEqual(1, 3-2)
def testNotEqual(self):
self.failUnlessEqual(2, 3-2)
if __name__ == '__main__':
unittest.main()
E quando questi test vengono eseguiti:
$ python unittest_notequal.py -v testEqual (__main__.InequalityTest) ... FAIL testNotEqual (__main__.InequalityTest) ... FAIL ====================================================================== FAIL: testEqual (__main__.InequalityTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_notequal.py", line 9, in testEqual self.failIfEqual(1, 3-2) AssertionError: 1 == 1 ====================================================================== FAIL: testNotEqual (__main__.InequalityTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unittest_notequal.py", line 12, in testNotEqual self.failUnlessEqual(2, 3-2) AssertionError: 2 != 1 ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=2)
Oltre alla stretta eguaglianza, è possibile verificare una "quasi" eguaglianza di numeri a virgola mobile usando
failIfAlmostEqual()
e
failUnlessAlmostEqual()
.
import unittest
class AlmostEqualTest(unittest.TestCase):
def testNotAlmostEqual(self):
self.failIfAlmostEqual(1.1, 3.3-2.0, places=1)
def testAlmostEqual(self):
self.failUnlessAlmostEqual(1.1, 3.3-2.0, places=0)
if __name__ == '__main__':
unittest.main()
I parametri sono i valori da confrontare, ed il numero di posizioni decimali da utilizzare per il test.
$ python unittest_almostequal.py .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
Come detto precedentemente, se un test solleva una eccezione diversa da
AssertionError
, viene trattata come un errore. Questo è molto utile per scoprire errori che si compiono mentre si sta modificando del codice per il quale già esiste un test abbinato. Ci sono circostanze, comunque, nelle quali si vuole eseguire il test per verificare che un certo codice effettivamente produca una eccezione. Ad esempio se un valore non valido viene passato come attributo di un oggetto. In tali casi,
failUnlessRaises()
rende il codice più chiaro che catturare l'eccezione nel proprio codice. Si confrontino questi due test:
import unittest
def raises_error(*args, **kwds):
print args, kwds
raise ValueError('Valore non valido: ' + str(args) + str(kwds))
class ExceptionTest(unittest.TestCase):
def testTrapLocally(self):
try:
raises_error('a', b='c')
except ValueError:
pass
else:
self.fail('Non si vede ValueError')
def testFailUnlessRaises(self):
self.failUnlessRaises(ValueError, raises_error, 'a', b='c')
if __name__ == '__main__':
unittest.main()
I risultati per entrambi sono gli stessi, tuttavia il secondo test, che usa
failUnlessRaises()
è più succinto.
$ python unittest_exception.py -v testFailUnlessRaises (__main__.ExceptionTest) ... ('a',) {'b': 'c'} ok testTrapLocally (__main__.ExceptionTest) ... ('a',) {'b': 'c'} ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
Fixtures sono risorse necessarie per un test. Ad esempio, se si stanno scrivendo parecchi test per la stessa classe, questi test necessitano tutti una istanza di quella classe da usere per il test. Altri impianti includono connessioni a database e file temporanei (molta gente potrebbe argomentare che usando risorse esterne i test non sono più considerabili a livello di unità, ma sono comunque test, e sono comunque utili).
TestCase
include uno speciale aggancio per configurare e ripulire un qualsivoglia impianto che sia necessario per i propri test. Per configurare gli impianti, si riscrive
setup()
. Per ripulire, si riscrive
tearDown()
.
import unittest
class FixturesTest(unittest.TestCase):
def setUp(self):
print 'In setUp()'
self.fixture = range(1, 10)
def tearDown(self):
print 'In tearDown()'
del self.fixture
def test(self):
print 'in test()'
self.failUnlessEqual(self.fixture, range(1, 10))
if __name__ == '__main__':
unittest.main()
Quando il test di esempio viene eseguito, si può vedere l'ordine di esecuzione dell'impianto e dei metodi di test:
$ python unittest_fixtures.py In setUp() in test() In tearDown() . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
La documentazione della libreria standard descrive come organizzare manualmente le raccolte di test. Io (Doug Hellmann - n.d.t.) generalmente non uso raccolte di test direttamente, poichè preferisco costruire le raccolte automaticamente (si tratta dopo tutto di test automatizzati). Automatizzare la costruzione di raccolte di test è specialmente utile per vaste basi di codice, nelle quali i test collegati non sono tutti nello stesso posto. Strumenti tipo nose facilitano la gestione dei test quando essi sono sparsi attraverso file e directory multiple.