Taula de continguts

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:

, , , , , , , , , , , , , , , , ,


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:


Test amb login

Mirem aquest exemple per logar-nos en el panell d'administració de la nostra app Django:

tests.py
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:

tests.py
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) )


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:

tests.py
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 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.

tests.py
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 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ó:

Versió amb assert i capturant NoSuchElementException

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).

tests.py
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

Versió amb Exception genèrica

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.

tests.py
    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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Crea un usuari amb permisos de «staff» però sense cap altre permís. Comprova que pots entrar a /admin i canviar la contrasenya.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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).
  15. 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.
  16. 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.
  17. 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.
  18. 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.