Testejant Django amb Nose
- aaloy
- 2 de abril de 2010
A poc a poc però sense pausa estic embarcat en la creació d'un motor de reserves orientat cap a hotels i cadenes hoteleres. És a dir, no es tracta de fer un sistema genèric orientat a la integració d'xml com els que poden necessitar agències i TTOO, sinó de tenir quelcom flexible i ràpid de personalitzar orientat a cobrir les necessitats més o manco complexes de la venda directa on line de nits d'hotel.
És a dir, el sistema ha de cobrir el bàsic (gestió del nombre d'habitacions disponibles, tarifes, descomptes per ocupació, aturada de vendes, etc) però també ha de permetre cobrir necessitat que en aquests moments no coneixem. Per tant, tenir una bona bateria de tests que ens assegurin que afegint noves característiques no ens estam carregant les que ja hi ha és fonamental.
La idea del Test Driven Development és que s'han d'escriure els tests abans d'escriure el codi. Jo no sóc tan purista i els tests els escric quan els necessit, unes vegades abans i unes vegades després d'escriure el codi. La raó és molt senzilla, quan estic immers en l'escriptura de codi per a que passi un test, sovint me trob afegint noves característiques per les que no tenc cap test encara. Llavors crec que el millor és seguir a la zona de programació pura i dura i després escriure els tests. Crec que no és tan important el moment en el que s'escriuen els tests unitaris com el fet de tenir-los.
Un motor de reserves com el que descric es pot fer en qualsevol llenguatge, en el meu cas hem triat fer-ho amb Python i amb Django, ja que ens dóna molta flexibilitat posterior a l'hora de fer adaptacions, que és el que cercam. Així doncs la definició del model de dades i l'ORM que s'utilitza és el de Django, la qual cosa fa que sigui important poder testejar-ho amb les eines que ens proporciona aquest bastiment.
Llegint la documentació de Django podem veure que aquest fa servir els units test de tota la vida i que a més per a que els tests siguin realment unitaris el que fa es utilitzar una base de dades nova i neta cada vegada que executam un test, d'aquesta manera ens asseguram que no hi ha dependències entre diferents execucions de casos de prova i per tant que els tests són realment unitaris respecte a les dades.
Tot i que sigui una solució totalment vàlida, consider que eines com nose fan l'escriptura de tests unitaris una tasca molt més divertida, ja que no has de passar pena de com estructura els test, sinó simplement els has d'escriure amb unes convencions de codi (per exemple els tests han de començar o contenir la paraula test). Per mi això significa menys complicacions i poder reaprofitar petits troços de codi que tanmateix hauria necessitat escriure per provar l'aplicació sense tenir que donar-los el formalisme que necessita un unit test. L'ideal per mi és tenir nose integrat dins el sistema de test de Django.
Afortunadament més gent ha tingut aquesta idea i per sort per mi, gent amb un coneixement més profund que jo de nose a nivell intern com per fer-ne una adaptació per Django, us present el django-nose. És una aplicació que s'instal·la amb pip o easy_install i que necessita molt poca configuració, afegirem 'django_nose'
al settings.py
a la secció INSTALLED_APPS
i afegirem TEST_RUNNER = 'django_nose.run_tests'
per dir-li a Django que farem servir el nostre motor de tests enlloc dels seus.
La gràcia d'aquest mòdul i de nose és que és compatible amb el que ja hi havia, però a més afegeix molta més funcionalitat i agilitat a l'hora de crear els tests. Basta fer un python manage.py test --help
per adonar-nos de tota la quantitat d'opcions que ens afegeix el mòdul a l'hora de testejar. D'aquestes m'agradaria destacar-ne algunes:
--with-coverage
Ens permet utilitzar la utilitat de coverage de Ned Batchelder que ens permetrà veure quines línies de codi no hem testejat encara. Amb opcions addicionals és capaç de generar-nos un html navegable per a que poguem veure exactament el context de les línies testejades i de les que queden sense provar.--processes
Ens permet aprofitar els nuclis del nostre ordinador, els tests s'executen en paral·lel (recordau que els tests unitaris no han de tenir dependències entre ells) accelerant notablement el temps de procés.--with-xunit
Fa que en lloc de tenir la sortida típica dels unit tests tenguen el format de xunit, el mateix que es fa servir als unit test de Java, per exemple, i que ens permetrà integrar els nostres unit tests amb eines d'integració contínua com Hudson.
A l'hora de testejar una aplicació com el motor de reserves és imprescindible partir de dades conegudes i controlades per poder determinar els resultats esperats. Per això la manera que té Django de carregar les dades és simple i elegant. A cada unit test i abans de l'execució de cada cas de prova es crea la base de dades (en el meu cas un sqlite3 en memòria) i es carreguen les dades inicials (o fixtures en la nomenclatura) que es defineixen a cada unit test. Els fitxtures no són més que arxius en format json, yaml o xml que representen els registres que hi ha d'haver dins la base de dades.
Per exemple: [{"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}} ]
Aquests arixus els podem crear nosaltres directament amb un editor com vim o bé aprofitar-nos de l'entorns de Django i crear-los a partir de l'admin. Així podem introduir les dades a la nostra base de dades i fer un
./manage.py dumpdata applicacio.model
per exemple:
./manage.py dumpdata sites.Site --indent=4
ens proporcionarà el codi json per al model Site, l'indent el que fa es proporcionar espaiat addicional per a que sigui més bo de llegir i de modificar.
Si ens va millor tractar amb yaml, és prou senzill canviar el format:
python manage.py dumpdata sites.Site --format=yaml --indent=4 - fields: {domain: example.com, name: example.com} model: sites.site pk: 1
Dins un mateix unit test podem carregar tants arixus de fixtures com vulguem, ja que basta definir una llista del que s'ha de carregar, això vol dir no tenir que tractar amb grans arixus de dades de proves, sinó poder-los fer molt més manejables i sobretot reaprofitables.
La combinació de fixtures, unit test de Python, eines de testeix de Django i nose és molt difícil de batre. Per una part tenim que escriure tests amb Python costa una fracció del temps i de codi respecte a escriure'ls en un altre llenguatge, però a més nose fa que la tasca sigui molt més natural i la capacitat de generar els fitxtures des del mananager de Django fa que una tasca avorrida es convertesqui en trivial.
Tenir una bona bateria de test és fonamental per provar l'aplicació, per demostrar que els casos que s'estan considerant funcionen i tenen les sortides que deim que han de tenir. Però la utilitat dels tests unitaris va més enllà. Són una eina imprescindible en la refactorització.
Una vegada l'aplicació ja fa el que volem arriba l'hora de plantejar-se si es pot fer millor, si l'aplicació pot ser més eficient, de refer el codi per a evitar repeticions. La regla fonamental de la refactorització és que no hem d'introduir funcionalitat nova, quan refactoritzam tot ha de funcionar com abans. Els tests unitaris ens ajuden a demostrar que la nostra refactorització és bona, en tant en quant passin exactament els mateixos tets que teníem abans de fer cap modificació.
Faceu Test Driven Development o agafeu una aproximació més pragmàtica, el cert és que tenir bons unit tests és una inversió de futur que costa sols un poc més de temps quan estam desenvolupant, ja que amb nose els tests es poden reaprofitar de les petites rutines per proves que tanmateix hauríem d'escriure. Els beneficis el veurem a mig i llarg plaç: quan refactoritzem, quan canviem codi, quan les proves les llanci un sistema d'integració contínua. És massa bo per a no fer-ho servir.