Test, test e ancora test
medio
Unit Testing
Lo sviluppo Test Driven, orientato ai test, ha dimostrato di essere una metodologia molto potente per rendere gli sviluppatori produttivi. Per questo motivo, scrivere test per ogni nuova funzionalità, per ogni modifica o correzione bug, da tempo viene fatto dalla maggior parte della Comunità di sviluppatori Zope/Plone: così tanto che se presentate del codice e desiderate che venga rilasciato, questo non accadrà senza i corrispondenti test!
Scrivere test richiede tempo, e devi prenderti il tempo di scrivere test. Una volta che li avrai scritti, quel tempo sarà ricompensato molte volte. Test ben progettati proveranno che le tue modifiche, correzioni di bug e rifattorizzazioni non hanno generato difetti nel tuo codice e ti eviteranno di perdere tempo a cercarli.
Il testing è magico, trasforma uno sviluppatore in un utente. Quando scrivi i tuoi test ti metti al posto dell'utente e cominci a immaginare come potrebbe usare il tuo codice. Questo può mostrarti quanto sia valido il tuo codice: dove sono le parti oscure, i metodi di difficile comprensione, le decomposizioni errate delle classi, etc.
Sai che uno dei punti di forza più grandi di Plone è la sua comunità. I test sono ancor più importanti per il codice condiviso perché:
- Desiderate condividere con altri le vostre funzionalità, il vostro modo di programmare, il vostro modo di pensare e mostrare il giusto modo di usare queste funzionalità.
- Molte persone diverse possono/potrebbero contribuire al vostro codice, se sono stati scritti molti test possono assicurarsi da soli che il codice che scrivono non rovina il vostro lavoro.
- Sono la tua garanzia quando il mantainer, o il responsabile del rilascio viene a brontolarti che hai rotto qualcosa con le tue ultime modifiche.
Quindi tutto questo serve alla verifica del tuo codice e a spiegare come funziona agli altri.
I test dovrebbero:
essere ripetibili;
funzionare senza il bisogno di intervento umano;
essere concisi;
raccontare una storia;
non verificare cose ovvie;
essere deterministici.
PloneTestCase
Quando crei test per Plone, non vuoi perdere tempo ad installare un portale Plone, o qualsiasi altro prodotto Zope/Plone. Perciò per ottenere risultati dei test più rapidi vengono usati framework per gli Unit Test capaci di creare suite di unit test automatizzati. Plone ha il suo: PloneTestCase.
Poichè Plone è basato su Zope, PloneTestCase è basato su ZopeTestCase (il framework per unit test di Zope - che è basata sul pacchetto di unit test del Python [e se desiderate conoscere tutta la storia, il pacchetto unit test del Python è basato su JUnit del Java e sul testing framework di Smalltalk). Questo framework è un aiuto enorme per far girare i vostri test velocemente, spesso e con risultati chiari.
Vocabolario
Test significa metodo di test
Unit test indica una classe che contiene tutti i metodi di test (se volete usare il test framework di Plone, questa classe dovrebbe ereditare da PloneTestCase).
Unit test suite indica una collezione di più unit test.
Descriveremo qui alcune delle funzionalità di base disponibili in PloneTestCase e che useremo molto nelle sezioni seguenti.
Prodotti installati
Questa è la lista dei prodotti di default installati. Se avete bisogno di prodotti supplementari dovrete installarli esplicitamente con il metodo che descriveremo successivamente.
Zope
ZCTextIndex
MailHost
PageTemplates
PythonScripts
ExternalMethod
GroupUserFolder
Five
CMF
CMFCore
CMFDefault
CMFCalendar
CMFTopic
DCWorkflow
CMFUid
CMFActionIcons
CMFQuickInstallerTool
CMFFormController
Plone - Archetypes
Archetypes
MimetypesRegistry
PortalTransfroms
ATContentTypes
ATReferenceBrowserWidget
CMFDynamicViewFTI
ExternalEditor
ExtendedPathIndex
ResourceRegistries
SecureMailHost
Kupu
- CMFPlone.
Con Plone 2.5 vengono installati anche altri prodotti importanti come CMFPlacefulWorkflow, PlonePAS…
Oggetti installati
Di seguito c'è una lista di oggetti che puoi usare quando installi PloneTestCare
self.portal : è un portale Plone pulito che si installa con tutti i portal_tool di cui avrete bisogno.
self.folder : quando attivi un PloneTestCase sei loggato in Plone come utente di default. Questa cartella (vuota) è la folder di default dell'utente. Poiché è importante che tu possa fare tutto quello che serve in questa folder, l'utente di default è il proprietario di questa cartella.
Metodi utili che puoi usare:
Questi sono i metodi che potete usare sull'oggetto self (all'interno dell'istanza di PloneTestCase):
addProduct(name): Usa quickinstaller per installare un prodotto all'interno del portale Plone (self.portal). Così se hai definito il tuo prodotto e il tuo Content Type, non dimenticarti di installarlo nel portale Plone prima di provare ad usarlo.
setRoles(roles, name=default_user) : Cambia i ruoli dell'utente corrente (i ruoli possono essere una stringa, un tuple o una lista). Veramente importante per controllare problemi di sicurezza. Potete anche cambiare i ruoli di altri utenti impostando il parametro name.
setGroup(groups, name=default_user) : Cambia i gruppi dell'utente corrente (i gruppi possono essere una stringa, un tuple o una lista). Potete anche cambiare i gruppi per altri utenti utilizzando il parametro name.
setPermissions(permissions, role) : Cambia i permessi sull'oggetto portale per il ruolo specificato. I permessi possono essere una stringa, un tuple o una lista. Il ruolo deve essere una stringa.
login(name) : A volte è più chiaro generare nuovi utenti con differenti ruoli o gruppi e dopo effettuare il login con questi utenti.
logout() : vuoi essere anonimo all'interno di Plone? Usa questo metodo.
Setup degli unit test
Il framework di testing manda in esecuzione tutti i metodi all'interno di qualunque classe se il nome del metodo comincia con la stringa test. Perciò testMethod1 (self) sarà automaticamente eseguito dal framework e non ti dovrai più preoccupare di invocarlo esplicitamente da qualche parte.
Potreste spesso voler ripetere la stessa fase di inizializzazione prima della chiamata al vostro metodo di test. Il framework ti offre un metodo efficace per farlo:
afterSetUp(self) : Dovresti mettere in questo metodo tutto il codice che vuoi mandare in esecuzione prima di eseguire ciascuno dei tuoi metodi di test.
Metodi di test di asserzione (basati su unittest del Python)
Nei test c'è un input conosciuto e un'uscita prevista. La correttezza di questa corrispondenza di ingresso e uscita è controllata da una asserzione. Il pacchetto di unit test del Python offre vari metodi per testare asserzioni:
failIf(expression) : il test fallisce se l' espressione è vera
failUnless(expression) : il test fallisce se l' espressione non è vera
failUnlessEqual(first, second) : Il test fallisce se i due oggetti non sono uguali secondo l' operatore
==.- failIfEqual(first, second) : il test fallisce se i due oggetti sono uguali secondo l'operatore
==. fail(msg) : Il test fallisce immediatamente, restituendo il messaggio specificato
failUnlessRaises(excClass, callableObj, args, *kwargs) : Il test fallisce a meno che un'eccezione di classe excClass non sia restituita da callableObj quando invocato con gli argomenti args e kwargs.
Fallimenti ed errori
Fallimenti ed errori sono due cose diverse!
Un fallimento accade quando un'asserzione fallisce (ti aspettavi il risultato opposto dall'asserzione del test).
Un errore accade quando succede qualcosa che non ti aspetti (eccezioni, errori nel codice…).
Creiamo un test! Si capisce meglio con un po' di pratica. Vedrai che è facile.
Il metodo usuale consiste nel creare una classe di test per ogni classe da testare ed un metodo (a volte di più) di test per ogni metodo importante nella vostra classe (i metodi getter e setter sono spesso esclusi dal test data la loro ovvietà).
In primo luogo scarica il PloneTestCase (http://plone.org/products/plonetestcase o da svn https://svn.plone.org/svn/collective/PloneTestCase/trunk/) ed estrailo nella cartella dei prodotti di Zope (supponiamo che avete installato Plone là dentro ;)).
Prendiamo la classe PloneTestCase con tutte le cose di cui abbiamo bisogno:
>>> from Products.PloneTestCase import PloneTestCase
Diciamo di voler testare parte del comportamento della classe Document di Plone (ATDocument). Generiamo una classe che usa questo PloneTestCase che abbiamo appena importato:
class TestATDocument(PloneTestCase):
"""
Un test case di base per Plone Document
"""
pass Questo è il nostro primo test di Plone. Facile no? Sono d'accordo che non testa molto :).
Testiamo quindi un paio di cose:
Quando modifico il titolo del mio documento, voglio che sia modificato correttamente (è vero che stiamo verificando funzionalità ovvioe, ma facciamo cose semplici).
Quando creo un documento, voglio che sia aggiunto all'interno del catalogo di Plone.
Come vedi in questi due test avremo bisogno di creare un documento, facciamolo una volta sola nel metodo di afterSetUp in modo che il nostro documento sia generato prima di ogni test.
Ricorda che il nome ogni metodo di test deve cominciare con la string test.
Generiamo il file testDocument.py:
class TestATDocument(PloneTestCase):
"""
Un test case meno basico per Plone Document
"""
def afterSetUp(self):
"""
Generiamo nella nostra cartella home il documento di cui abbiamo bisogno
"""
self.folder.invokeFactory('Document', id='doc')
# Ora abbiamo un documento con id "doc" dentro alla nostra cartella home
def testEditTitle(self):
"""
Vediamo se la modifica del titolo del documento va a buon fine
"""
self.folder.doc.setTitle('A wonderful document title')
self.assertEqual(self.folder.doc.Title, 'A wonderful document title')
# questo fallirà se il metodo setTitle non lavora correttamente!
def testDocumentInCatalog(self):
"""
Vediamo se il documento è presente nel catalogo
"""
# il catalogo è nel portale Plone
self.failUnless(self.portal.portal_catalog(getId='doc')) Eccolo qui. Se il test passa possiamo essere certi di poter cambiare il titolo di un documento correttamente e che una volta creato un documento è nel catalogo di Plone.
Ora è venuto il momento di includere il nostro nuovo testcase all'interno di un testsuite, e di eseguire i nostri test.
Per potere fare questo avrete bisogno di due file:
framework.py : Per poter lanciare il test dal Python avrai bisogno di impostare alcune PATH, questo file farà la maggior parte del lavoro per voi.
runalltests.py : Questo piccolo script Python si limita ad eseguire tutti i file nella directory corrente che iniziano con la parola “test”.
Copia questi file dalla cartella di PloneTestCase alla cartella in cui sono tutti i tuoi test case (spesso nella cartella “tests”).
Quindi, per eseguire la suite di test, dovremo decorare il nostro PloneTestCase. Per aggiungere un po' di difficoltà, voglio inoltre installare un prodotto all'interno del mio portale che non è presente tra quelli dichiarati. Diciamo che desidero utilizzare il Plone Language Tool (ammetto che non ne avremo bisogno per l'esecuzione della nostra prova):
# Prima, sopra a tutto, mandiamo in esecuzione il framework.py
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
# Installiamo il prodotto PloneLanguageTool in Zope
from Testing import ZopeTestCase
ZopeTestCase.installProduct('PloneLanguageTool')
# Inizializziamo il nostro Plone e installiamo di default il prodotto PloneLanguageToolal suo interno
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite(products=['PloneLanguageTool'])
# Ecco fatto, tutto installato. Possiamo mettere qui il nostro testcase...
class TestATDocument(PloneTestCase.PloneTestCase):
"""
Un test case meno semplice per Plone Document
"""
def afterSetUp(self):
"""
Generiamo nella nostra cartella home il documento di cui abbiamo bisogno
"""
self.folder.invokeFactory('Document', id='doc')
# Ora abbiamo un documento con id "doc" dentro alla nostra cartella home
def testEditTitle(self):
"""
Vediamo se la modifica del titolo del documento va a buon fine
"""
self.folder.doc.setTitle('A wonderful document title')
self.assertEqual(self.folder.doc.Title(), 'A wonderful document title')
# questo fallirà se il metodo setTitle non lavora correttamente!
def testDocumentInCatalog(self):
"""
Vediamo se il documento è presente nel catalogo
"""
# il catalogo è nel portale Plone
self.failUnless(self.portal.portal_catalog(getId='doc'))
# Ora dobbiamo mettere il nostro testcase in una test suite.
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestATDocument))
return suite
# e se vuoi essere in grado di eseguire la tua suite direttamente (python testDocument.py)
if __name__ == '__main__':
framework() Ora tutto è predisposto. L'ultima cosa da fare è dire dove è posizionato il vostro Zope nel sistema. Su sistemi Unix potete fare così:
export SOFTWARE_HOME=/usr/lib/zope2.9/lib/python
Ora avete due modi per eseguire il vostro test suite:
"python runalltests.py" : che cerca tutti i file con il nome che comincia con “test” ed esegue tutte le test suite definite.
“python testDocument.py" : che esegue la specifica test suite.
Mentre in esecuzione vedrai
Installazione dei prodotti Zope
Una volta eseguito, un singolo test (metodo) sarà rappresentato da:
"." : che significa che il test è stato eseguito correttamente
"F" : che indica che il tuo test è fallito (otterrete maggiori informazioni alla fine)
"E" : che significa che il tuo test ha un errore (otterrete maggiori informazioni alla fine)
Ogni volta che un test fallisce o ha un errore otterrete un traceback e informazioni più dettagliate sul fallimento/errore.
DocTest
Cosa vedi nella parola DocTest? Doc e Test. Quindi un doctest è documentazione e allo stesso tempo un test che verifica che il tuo codice funziona.
Molti sostengono che la gente dovrebbe leggere gli unit test, che dovrebbero essere abbastanza chiari da non dovere aggiungere ulteriori commenti. È corretto che i test dovrebbero essere chiari ma non sarei troppo rigido. Penso che spiegando il mio codice a più persone possibile otterrò un maggior numero di feedback.
Anche se consideriamo i test case come documentazione per lo sviluppatore, i DocTest sono considerati una via di mezzo fra documentazione e test case. Mai più documentazione sorpassata ed inutile! Doctest permette di rendere viva la documentazione, sempre al passo con l'implementazione corrente.
Un doctest è un file di testo, o di testo strutturato (che dovrebbe essere scritto all'interno della cartella “docs” dei tuoi pacchetti/prodotti). Quindi all'interno di questo file spiegherai il tuo codice ed allo stesso tempo potrai invocare codice Python. Per invocare codice Python basta scrivere:
>>>
Ciò rappresenta una chiamata all'interprete Python. Intorno ad esso potete disporre la vostra spiegazione. Se il vostro codice Python restituisce qualcosa, dovete fare esattamente lo stesso come se aveste denominato questo codice da una sessione dell' interprete del Python. Per esempio:
>>> print 'hello world' hello world
Il valore di ritorno deve essere scritto allo stesso livello di indentazione del >>>
Un problema è che un doctest rappresenta in sé più di un test. Vuoi mostrare molte cose all'interno del tuo doctest, ma il metodo afterSetUp viene eseguito soltanto una volta prima dell'esecuzione del doctest. Un doctest rappresenta una sessione Python. Così se scrivo:
>>> a = 'hello'
La mia variabile a sarà impostata a “hello” fino alla fine del documento. Non dimenticarlo mai, potrebbe comportare grossi problemi!
A proposito, tutto questo documento è un doctest per i prodotti di ATContentTypes. Può essere eseguito in quel contesto.
Vocabolario
Un “file” doctest rappresenterà un file di testo che include il nostro doctest.
Una “classe” doctest rappresenterà la classe unit test che definisce un doctest.
Una volta scritto, il file doctest dovrebbe essere collegato ad una classe test case e ad una test suite. Perciò vediamo come si prepara un doctest nella parte del test (questo dovrebbe andare all'interno di un file Python nella cartella dei test - con un nome file che comincia con test):
# Come prima usiamo il framework.py
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
# Installiamo plone come solito. Vogliamo testare cose attinenti a plone
# nel nostro doctest
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite()
# quindi abbiamo bisogno della doctestsuite di zope e di linkare il nostro file doctest
# con un test case funzionale
from Testing.ZopeTestCase import FunctionalDocFileSuite
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
# abbiamo un file doctest chiamato archive.txt che è posizionato in
# ATContentTypes nella cartella docs (lo ripeto, il file doctest
# dovrebbe sempre trovarsi nella cartella docs, non nella cartella dei test).
def test_suite():
import unittest
suite = unittest.TestSuite()
suite.addTest(FunctionalDocFileSuite('archive.txt',
package="Products.ATContentTTypes.docs",
test_class=FunctionalTestCase
)
)
if __name__ == '__main__':
framework() Come al solito puoi mandare in esecuzione direttamente questo file, o semplicemente eseguire python runalltests.py.
Ora immagina di voler predisporre alcune cose all'interno della tua classe testcase prima di eseguire il file doctest (archive.txt). È facile, genera la tua classe di test ereditando da FunctionalTestCase, definisci il metodo afterSetUp e modifica la test suite in modo da usare la tua classe. Facciamolo…
Fate attenzione, questo potrebbe confondere la gente che legge il vostro file doctest se non spiegate chiaramente che già avete generato i test nella classe doctest.
Manteniamolo semplice. Immaginiamo di dover realmente generare un documento all'interno della cartella home, ma non abbiamo bisogno di mostrarlo nel file doctest:
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
from Products.PloneTestCase import PloneTestCase
PloneTestCase.setupPloneSite()
# ora ereditiamo FunctionalTestCase e definiamo il nostro metodo afterSetUp
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
class TestArchiveWithDocument(FunctionalTestCase):
"""
La nostra classe di test funzionale con un documento al suo interno
"""
def afterSetUp(self):
"""
Creazione di un documento nella cartella home che il doc test
archive possa usare
"""
self.folder.invokeFactory('Document', id='doc')
# Questo è tutto! Nessun metodo di test, il nostro unico metodo di test sarà
# il nostro file doctest.
# ora abbiamo bisogno di collegare la nostra classe di test funzionale
# al nostro file doctest all'interno di una test suite:
from Testing.ZopeTestCase import FunctionalDocFileSuite
def test_suite():
import unittest
suite = unittest.TestSuite()
suite.addTest(FunctionalDocFileSuite('archive.txt',
package="Products.ATContentTTypes.docs",
test_class=TestArchiveWithDocument
)
)
if __name__ == '__main__':
framework() Ora conoscete tutto sui test, non avrete più giustificazioni se non ne scrivete!
