5.3.2 Organizzare il codice di test

I componenti di base che costituiscono gli unit testing sono i test case (NdT: I ``casi da testare'') -- singoli scenari che devono essere costruiti e testati per provarne la correttezza. In PyUnit, i test case sono rappresentati da istanze della classe TestCase nel modulo unittest. Per costruire dei vostri test case dovete scrivere delle classi derivate da TestCase o usare FunctionTestCase.

Un'istanza di una classe derivata da TestCase è un oggetto che può eseguire completamente un singolo metodo di test, insieme a codice facoltativo per l'ordinamento e l'impostazione.

Il codice da testare di una istanza TestCase dovrebbe esservi interamente contenuto, così che possa essere eseguito sia da solo che in una combinazione arbitraria con un numero qualsiasi di altri test case.

La più semplice sotto classe di test case semplicemente ridefinisce il metodo runTest() per permettere di eseguire del codice di test specifico:

import unittest

class DefaultWidgetSizeTestCase(unittest.TestCase):
    def runTest(self):
        widget = Widget("The widget")
        self.failUnless(widget.size() == (50,50), 'dimensione predefinita errata')

Notate che per testare qualcosa, si usa uno dei metodi assert*() o fail*(), messi a disposizione dalla classe base TestCase. Se il test fallisce quando il caso da testare viene eseguito, viene sollevata un'eccezione e l'ambiente di test identifica il caso da testare come failure. Altre eccezioni che non vengono sollevate dai controlli fatti attraverso i metodi assert*() e fail*() sono identificate come errors dall'ambiente di test.

Il modo per eseguire un caso da testare sarà descritto in seguito. Per ora, notate che per costruire un'istanza di questi test case si chiama il suo costruttore senza argomenti:

testCase = DefaultWidgetSizeTestCase()

Tali test case possono essere numerosi e la loro impostazione può essere ripetitiva. Nel caso precedente, la costruzione di un ``Widget'' in ognuno dei 100 Widget della classe derivata dei test case porta ad una sgradevole duplicazione.

Fortunatamente, possiamo automatizzare questo istanziamento implementando un metodo chiamato setUp() che il nostro ambiente di test chiamerà automaticamente quando verrà eseguito il test:

import unittest

class SimpleWidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget("Il widget")

class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
    def runTest(self):
        self.failUnless(self.widget.size() == (50,50),
                        'dimensione predefinita errata')

class WidgetResizeTestCase(SimpleWidgetTestCase):
    def runTest(self):
        self.widget.resize(100,150)
        self.failUnless(self.widget.size() == (100,150),
                        'dimensione errata dopo il ridimensionamento')

Se il metodo setUp() riscontra una eccezione durante l'esecuzione del test, l'ambiente considererà il test come non riuscito e non sarà eseguito il metodo runTest().

Analogamente, possiamo prevedere un metodo tearDown() che si avvia dopo l'esecuzione del metodo runTest():

import unittest

class SimpleWidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget("Il widget")

    def tearDown(self):
        self.widget.dispose()
        self.widget = None

Se setUp() va a buon fine, il metodo tearDown() sarà eseguito senza preoccuparsi se runTest() abbia avuto o meno successo.

Un tale ambiente di lavoro per testare il codice è chiamato fixture.

Spesso, molti piccoli test case useranno la medesima fixture. In questo caso, faremmo sfociare la classe derivata SimpleWidgetTestCase in piccole classi monometodo tipo la DefaultWidgetSizeTestCase. Questa pratica è una perdita di tempo ed è deprecata, visto che è sulla falsariga di JUnit, PyUnit fornisce un meccanismo più semplice:

import unittest

class WidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget("Il widget")

    def tearDown(self):
        self.widget.dispose()
        self.widget = None

    def testDefaultSize(self):
        self.failUnless(self.widget.size() == (50,50),
                        'dimensione predefinita errata')

    def testResize(self):
        self.widget.resize(100,150)
        self.failUnless(self.widget.size() == (100,150),
                        'dimensione errata dopo il ridimensionamento')

Qui non abbiamo fornito un metodo runTest(), ma abbiamo invece fornito due metodi di test diversi. Le istanze di classe ora eseguiranno ciascuna uno dei metodi test*(), con self.widget che verrà creato e distrutto separatamente per ciascuna istanza. Quando creiamo un'istanza, dobbiamo specificare quale metodo di test eseguire. Questo si fa dichiarando il nome del metodo nel costruttore:

defaultSizeTestCase = WidgetTestCase("testDefaultSize")
resizeTestCase = WidgetTestCase("testResize")

Le istanze dei test case sono raggruppate assieme tenendo conto delle caratteristiche che vanno a testare. PyUnit fornisce un meccanismo per questo: la test suite (NdT: ``insieme di test''), rappresentata dalla classe TestSuite nel modulo unittest:

widgetTestSuite = unittest.TestSuite()
widgetTestSuite.addTest(WidgetTestCase("testDefaultSize"))
widgetTestSuite.addTest(WidgetTestCase("testResize"))

Per facilitare l'esecuzione del test, come si vedrà più avanti, è buona idea inserire in ciascun modulo di test un oggetto richiamabile che restituisca una test suite preconfezionata:

def suite():
    suite = unittest.TestSuite()
    suite.addTest(WidgetTestCase("testDefaultSize"))
    suite.addTest(WidgetTestCase("testResize"))
    return suite

o addirittura:

class WidgetTestSuite(unittest.TestSuite):
    def __init__(self):
        unittest.TestSuite.__init__(self,map(WidgetTestCase,
                                              ("testDefaultSize",
                                               "testResize")))

(Il secondo è ammesso per coloro che non siano deboli di cuore!)

Visto che è un comportamento comune creare una classe derivata di TestCase con molte funzioni di test dal nome simile, esiste una utile funzione, chiamata makeSuite() che costruisce una test suite contenente tutti i test case presenti in una classe test case:

suite = unittest.makeSuite(WidgetTestCase)

--

Notate che quando si utilizza la funzione makeSuite(), l'ordine di esecuzione dei vari test case verrà determinato ordinando i nomi delle funzioni tramite la funzione built-in cmp().

Spesso è desiderabile raggruppare insieme le test suite, in modo da eseguire in una volta i test per l'intero sistema. Questo è facile, visto che le istanze di TestSuite possono essere aggiunte a TestSuite proprio come istanze di TestCase possono essere aggiunte a TestSuite:

suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite((suite1, suite2))

Potete mettere le definizioni dei test case e delle test suite nello stesso modulo in cui si trova il codice che essi andranno a testare (per esempio widget.py), ma mettere il codice da testare in un modulo separato, come widgettests.py, porta diversi vantaggi:

Vedete Circa questo documento... per informazioni su modifiche e suggerimenti.