L'elaboració de tests automatitzats és molt important de cara al desenvolupament i manteniment del programari. Un projecte sense tests no serà acceptat per una comunitat de desenvolupadors que puguin contribuir (si estem parlant d'un projecte de codi lliure).
El paradigma TDD o Test Driven Development estableix que abans de desenvolupar un codi cal elaborar els tests funcionals pertinents que asseguren el bon funcionament del programari. És molt possible que el codi dels tests s'estengui molt més encara que el codi de la pròpia aplicació. Pot semblar una mala pràctica però no ho és: uns bons tests asseguren el bon funcionament actual i futur.
Referències:
Per crear tests cal crear, a l'arxiu tests.py
, una classe derivada de TestCase
amb el nom que vulguem. Els mètodes amb un nom que comenci per test_
s'executaran com a test independent dels altres. Això significa que cada test començarà amb la mateixa configuració de BD, i si creem elements (com per exemple, un usuari), aquests només seran vàlids durant el test en curs, i no es «propagaran» als altres tests.
Els mètodes que no comencin per test_
no s'executaran per part del framework. Els podem fer servir nosaltres com a helpers per a el què ens convingui, i els podem cridar en qualsevol moment.
Hi ha 2 mètodes especials:
setUp(self)
: es crida abans de començar els tests. Ens deixaran una configuració que serà la mateixa per a tots els tests de la classe. Si hem creat un usuari, aquest persistirà durant tots els tests.tearDown(self)
: es crida quan s'han acabat tots els tests.
Mirem aquest exemple per logar-nos en el panell d'administració de la nostra app Django:
from django.test import TestCase from django.urls import reverse # Aquest és el model de User habitual en un projecte estàndard from django.contrib.auth.models import User # Aquest altre és el model alternatiu quan tenim un User personalitzat #from django.contrib.auth import get_user_model #User = get_user_model() class LoginTests(TestCase): def test_create_superuser(self): # Creem un superusuari User.objects.create_superuser('admin', '[email protected]', 'admin123') # ens loguem amb username (o amb email, segons es configuri) login = self.client.login(username='admin',password='admin123') self.assertTrue(login) # visitem la pàgina principal del panell de control /admin response = self.client.get('/admin/') # comprovem que el login és correcte si apareix "Benvingut/da" al HTML self.assertTrue( "Welcome" in str(response.content) ) self.assertTrue( "Log out" in str(response.content) ) def test_segon(self): # el superusuari creat al test anterior aquí ja no existeix # aquesta sentència fallarà login = self.client.login(username='admin',password='admin123') # no fem assertTrue de "login" per no provocar un test fallit
Els tests es poden posar en marxa amb:
(env) $ ./manage.py test
L'usuari creat per al test_create_superuser
només estarà actiu durant aquest test. Quan s'acabi el test (al finalitzar la funció) la BD farà un rollback i tot el creat es destruirà, per tal de començar el següent test en la següent funció amb la BD «neta».
Si volem que l'usuari creat estigui disponible per a tots els tests, cal posar-ho en la funció setUp
:
from django.test import TestCase from django.urls import reverse from django.contrib.auth.models import User class LoginTests(TestCase): def setUp(self): # Creem un superusuari vàlid per a tots els tests User.objects.create_superuser('admin', '[email protected]', 'admin123') # si ens loguem al setUp, també el client estarà logat per a tots els tests login = self.client.login(username='admin',password='admin123') self.assertTrue(login) def test_superuser(self): # visitem la pàgina principal del panell de control /admin response = self.client.get('/admin/') # comprovem que el login és correcte si apareix "Benvingut/da" al HTML self.assertTrue( "Welcome" in str(response.content) ) def test_segon(self): # visitem la pàgina principal del panell de control /admin response = self.client.get('/admin/') # comprovem que el login és correcte si apareix el logout al HTML self.assertTrue( "Log out" in str(response.content) )
De vegades es pot fer massa llarg crear tots els objectes d'una BD dintre dels tests. Llavors ens és útil crear una BD amb les dades bàsiques per realitzar els tests més avançats. Per a fer això amb Django només cal elaborar les dades que ens calguin a una BD de proves, i volcar-la en un arxiu JSON mitjançant l'ordre dumpdata
.
Crea una DB (migrate), afegeix les dades que necessitis (createsuperuser), i volca els continguts a un arxiu de dades de test:
(env) $ ./manage.py migrate ... (env) $ ./manage.py createsuperuser ... (env) $ ./manage.py dumpdata --natural-foreign --natural-primary > testdb.json
Un cop tinguem les dades a l'arxiu, podem realitzar els tests indicant l'arxiu de dades de test a l'atribut fixtures
del test:
from django.test import TestCase from django.urls import reverse from django.contrib.auth.models import User class LoginTests(TestCase): fixtures = ('testdb.json',) def setUp(self): # si ens loguem al setUp, també el client estarà logat per a tots els tests login = self.client.login(username='admin',password='admin123') self.assertTrue(login) def test_superuser(self): # visitem la pàgina principal del panell de control /admin response = self.client.get('/admin/') # comprovem que el login és correcte si apareix "Benvingut/da" al HTML self.assertTrue( "Welcome" in str(response.content) ) def test_segon(self): # visitem la pàgina principal del panell de control /admin response = self.client.get('/admin/') # comprovem que el login és correcte si apareix el logout al HTML self.assertTrue( "Log out" in str(response.content) )
Selenium és una eina per automatitzar l'ús del browser. És ideal per a realitzar tests funcionals automatitzats.
A la documentació oficial de Django s'explica com treballar amb Selenium.
Necessitaràs instal·lar el geckodriver de Firefox per permetre automatitzar l'accés al navegador per part del codi (control remot). Des de fa algunes versions Firefox ja inclou per defecte el driver.
Per altra banda hem detectat que les versions de Firefox de la botiga snap d'Ubuntu no funcionen bé per al testing. Per tant, si utilitzes aquesta distribució caldrà eliminar el Firefox normal i després instal·lar Firefox ESR (Extended Support Release). Debian ja porta per defecte Firefox ESR.
$ sudo snap remove firefox $ sudo apt install firefox-esr
També cal instal·lar Selenium al virtualenv del nostre projecte:
(env) $ pip install selenium
Mostrem un exemple de com es pot connectar amb el panell /admin i fer un login. Hi hem afegit un control sobre el mode headless que ens permet córrer els tests sense mostrar el browser. Ens anirà bé per poder executar els tests en servidors sense interfície gràfica.
from django.contrib.staticfiles.testing import StaticLiveServerTestCase from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.common.by import By class MySeleniumTests(StaticLiveServerTestCase): fixtures = ['testdb.json',] @classmethod def setUpClass(cls): super().setUpClass() opts = Options() #opts.headless = True # DEPRECATED! cls.selenium = WebDriver(options=opts) cls.selenium.implicitly_wait(5) @classmethod def tearDownClass(cls): # no sortim el browser per comprovar visualment com ha anat #cls.selenium.quit() super().tearDownClass() def test_login(self): self.selenium.get('%s%s' % (self.live_server_url, '/admin/login/')) # comprovem que el títol de la pàgina és el què esperem self.assertEqual( self.selenium.title , "Log in | Django site admin" ) # introduïm dades de login i cliquem el botó "Log in" per entrar username_input = self.selenium.find_element(By.NAME,"username") username_input.send_keys('admin') password_input = self.selenium.find_element(By.NAME,"password") password_input.send_keys('admin123') self.selenium.find_element(By.XPATH,'//input[@value="Log in"]').click() # comprovem que hem entrat al panell d'administració pel títol self.assertEqual( self.selenium.title , "Site administration | Django site admin" ) # Aquesta localització de l'element ens serveix també a mode de ASSERT # Si no localitza el link "Log out", ens donarà un NoSuchElementException self.selenium.find_element(By.XPATH,"//button[text()='Log out']")
El mode headless és important ja que ens permetrà que el navegador funcioni sense el GUI, cosa que necessitem per als testos automatitzats (els servidors de test solen ser servidors sense interfície gràfica, pel què intentar obrir un navegador real fallaria al no trobar les X-windows).
Fixa't en què es pot activar i desactivar el mode headless del driver del navegador Firefox. Si vols inhibir la interfície gràfica del Firefox, ho notifiquem a Selenium a través de les variables d'entorn:
(env) $ MOZ_HEADLESS=1 ./manage.py tests
De vegades ens interessarà veure el resultat dels tests amb el GUI, típicament per depurar i corregir els bugs quan els tests fallin.
En l'exemple vist es veu com comprovar que un element HTML (en l'exemple, un button
) sí que existeix:
# Aquesta localització de l'element ens serveix també a mode de ASSERT # Si no localitza l'element, llençarà una NoSuchElementException self.selenium.find_element(By.XPATH,"//button[text()='Log out']")
Per saber més de Selenium i les seves funcions pots mirar la documentació oficial de Selenium per Python on veuràs més funcions de com accedir al DOM i manipular els seus events (click, right click, send keys etc).
Però què passa si volem comprovar que l'element NO existeix?
Un possible truc és cercar-ho i capturar l'excepció:
En aquesta versió, si el find_element
funciona, forcem un AssertionError
(l'element no s'hauria d'haver trobat).
El except
només captura si és una excepció NoSuchElementException
de Selenium, i en tal cas no farem res i seguim amb l'execució (l'element no hi és).
from selenium.common.exceptions import NoSuchElementException #... try: self.selenium.find_element(By.XPATH,"//a[text()='Log out']") assert False, "Trobat element que NO hi ha de ser" except NoSuchElementException: pass
En aquesta versió, capturem totes les excepcions, i en cas que no n'hi hagi cap, llancem una Eception
genèrica o bé forcem el AssertionError
.
try: self.selenium.find_element(By.XPATH,"//a[text()='Log out']") except: pass else: raise Exception("Trobat element que NO hi ha de ser") # o bé assert False, "Trobat element que NO hi ha de ser"
Realitza un dels següents tests per a Django. El professor te n'assignarà un de concret.
La BD de test testdb.json
només ha de contenir un usuari superadmin i res mes. Tot el què diu l'exercici s'ha de fer amb codi.
/admin
no pot entrar./admin
sí que pot entrar./admin
, i que quan intentem crear un usuari ens apareix entre els grups que li podem assignar./admin
i canviar la contrasenya./admin
i comprova que hi ha el botó «view site» i que ens porta a una pàgina vàlida (codi 200). Si no hi tens cap pàgina vàlida a l'arrel, crea'n una de senzilla.Question
, i 2 Choice
per a cada Question
des del menú «Add Choice». Comprova que quan accedeixes a una Question
, t'apareixen les seves 2 Choice
als formularis inline.Question
. Crea 2 Choice
per a cada Question
dins del menú inline de cadascuna d'elles. Comprova que al menú «Choices» pots veure les 4 Choice
creades.Question
i Choice
. Crea 2 Question
amb 2 Choice
cadascuna amb el superusuari. Comprova que el nou usuari pot entrar i veure les Question
però que no les pot editar.Question
amb 1 Choice
i una Question
amb 100 Choices
(amb un bucle, òbviament) des del menú inline
. Genera un text aleatori per cadascuna d'elles. Comprova que anant al menú de Choices
pots visualitzar les 101 opcions.Questions
ni Choices
al menú de l'admin panel.