====== Tests amb Django ====== 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: * [[Django]] en aquesta wiki. * Pàgina oficial [[https://docs.djangoproject.com/en/3.2/topics/testing/|Testing in Django]] * [[https://realpython.com/testing-in-django-part-1-best-practices-and-examples/|Una pàgina interessant sobre testing amb Django]]. {{ youtube>qwypH3YvMKc }} {{tag> #FpInfor #Ciber #CiberMp03 #Ceti #CetiMp03 #Daw #DawMp07 #DawMp07Uf2 #DawMp07Uf02 #DawMp07Uf04 #DawMp07Uf4 django test testing DevOps python_web web framework}} \\ ===== Crear tests ===== 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. \\ ===== Test amb login ===== 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', 'admin@borsa.com', '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', 'admin@borsa.com', '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) ) \\ ===== Utilitzant una DB de test ===== 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) ) \\ ===== Tests amb Selenium ===== Selenium és una eina per automatitzar l'ús del //browser//. És ideal per a realitzar tests funcionals automatitzats. A la [[https://docs.djangoproject.com/en/4.0/topics/testing/tools/#liveservertestcase|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']") ==== Mode Headless ==== 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. ===== Testejar que un element NO existeix ===== 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 [[https://selenium-python.readthedocs.io/locating-elements.html|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" \\ ===== Exercicis de test amb Django Tutorial ===== 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. - Crea un usuari sense cap grup ni permís especial. Comprova que apareix a la llista d'usuaris però que si intenta logar-se al ''/admin'' no pot entrar. - Crea un usuari amb permisos de "staff". Comprova que apareix a la llista d'usuaris i que si intenta logar-se al ''/admin'' sí que pot entrar. - Crea un grup. Comprova que apareix a la llista de grups del ''/admin'', i que quan intentem crear un usuari ens apareix entre els grups que li podem assignar. - Crea un usuari amb permisos de "staff" però sense cap permís ni grup especial. Comprova que al logar-te no pots crear altres Users ni Questions. - Crea un usuari amb permisos de "staff" i amb permisos per a crear i visualitzar Questions. Comprova que al logar-te sí pots crear Questions però no Users. - Crea un usuari amb permisos de "staff" i amb permisos per a crear i visualitzar Usuaris. Comprova que al logar-te sí pots crear Users però no Questions. - Crea un usuari amb permisos de "staff" però sense cap altre permís. Comprova que pots entrar a ''/admin'' i canviar la contrasenya. - Entra amb qualsevol usuari al ''/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. - Crea un usuari amb permisos de "staff" i amb permisos per visualitzar Usuaris. Comprova que sí que pot veure'ls però no pot crear-ne ni esborrar. - Crea un usuari amb permisos de "staff" i amb permisos per visualitzar Questions. Comprova que sí que pot veure-les però no pot crear-ne ni esborrar. - Crea 2 //groups// de Django: profe i alumne. Crea 2 usuaris, un de cada tipus. Assigna permisos de "staff" als "profes" i cap permís al grup "alumne". Comprova que l'usuari del grup "profe" pot entrar al panell /admin, però l'alumne no pot entrar. - Entra al panell /admin i crea 2 ''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//. - Entra al panell /admin i crea 2 ''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. - Crea un usuari amb permisos de "staff". Entra amb aquest usuari a l'//admin panel// i comprova que si vol canviar la contrasenya, les restriccions de contrasenya segura funcionen (les 4 restriccions que s'enuncien al formulari, cal fer-les aparèixer). - Crea un usuari amb permisos de "staff" i que pugui llegir (només llegir) les ''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. - Crea un usuari amb permisos de "staff" i amb permís per veure (només veure) els usuaris. Crea 3 usuaris sense permisos amb el superadmin i comprova que el nou usuari pot veure'ls però no editar-los. - Crea una ''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. - Crea un usuari amb permisos de //staff// però sense permisos explícits ni grups. Comprova que quan entra no pot veure ni ''Questions'' ni ''Choices'' al menú de l'//admin panel//.