Un codi ha de tenir un conjunt de proves automatitzades que et permeten dissenyar nou codi i poder modificar codi verificant que tot segueix funcionant correctament.
Introducció
Per escriure un programa has de fer proves de que aquest funciona.
Assert
Crea el fitxer test.py
:
msg = "Hello World!"
hello = msg[:5]
print(hello)
La manera antiga antiga de provar un codi era fer un "print" i mirar que el resultat que apareix per pantalla és el que esperaves.
$ python3 test.py
Hello
Enlloc de fer un print i mirar per pantalla, pots fer un "assert" del resulta esperat:
msg = "Hello World!"
hello = msg[:5]
assert hello == "Hello"
Si tot va bé, no veurás res:
$ python3 test.py
Però si hello
no és "Hello"
perqué t'has equivocat:
msg = "Hello World!"
hello = msg[:3]
assert hello == "Hello"
Veurás un missatge d'error:
$ python3 test.py
Traceback (most recent call last):
File "/home/box/py/test.py", line 4, in <module>
assert hello == "Hello"
^^^^^^^^^^^^^^^^
AssertionError
Test-Driven development
Modifica el fitxer test.py
.
Afegeix una llista de persones:
persons = [
{"nom": "Eva", "cognom": "Vilaregut", "sexe": "Dona", "edat": 19, "altura": 162},
{"nom": "Joan", "cognom": "Sales", "sexe": "Home", "edat": 25, "altura": 173},
{"nom": "Raquel", "cognom": "Viñales", "sexe": "Dona", "edat": 12, "altura": 123},
{"nom": "Esther", "cognom": "Parra", "sexe": "Dona", "edat": 33, "altura": 178},
{"nom": "Miquel", "cognom": "Amorós", "sexe": "Home", "edat": 56, "altura": 166},
{"nom": "Laura", "cognom": "Casademunt", "sexe": "Dona", "edat": 41, "altura": 182},
]
Anem a escriure un codi que torni el nom de la persona més alta de la llista.
Enlloc d'escriure el codi que busca la persona més alta, el primer que has de fer és escriure el test:
assert result["nom"] == "Laura"
Si executes el codi tindras un error bastant evident:
$ pyhton3 test.py
Traceback (most recent call last):
File "/home/box/py/test.py", line 10, in <module>
assert result["nom"] == "Laura"
^^^^^^
NameError: name 'result' is not defined
La variable result
no està definida.
Et pot semblar una mica absurd, però l'art de la programació consisteix en poder avançar en passos molt petits quan és necessari.
L'error t'indica el que has de fer a continuació, definir una variable result
on es guardarà el resultat.
result = {}
assert result["nom"] == "Laura"
Al execurat el codi ara l'error serà diferent:
$ python3 test.py
Traceback (most recent call last):
File "/home/box/py/test.py", line 12, in <module>
assert result["nom"] == "Laura"
~~~~~~^^^^^^^
KeyError: 'nom'
La variable result
no té la clau "nom".
El que podem fer és afegir la primera persona de la llista com a resultat (si el seu nom no és "Laura"!):
result = persons[0]
assert result["nom"] == "Laura"
Al executar el codi l'error serà de que el resultat és l'"Eva" i no pas la "Laura".
$ python3 test.py
box@python:~/py$ /bin/python3 /home/box/py/test.py
Traceback (most recent call last):
File "/home/box/py/test.py", line 12, in <module>
assert result["nom"] == "Laura"
^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
El que hem de fer ara és recòrrer tots els elements de la llista amb un for
:
result = {}
for person in persons:
result = person
assert result["nom"] == "Laura"
Ara el test funciona perquè la "Laura" és l'últim element de la llista!
Que un test funcioni no vol dir que el codi estigui ben implementat.
Modifica la llista de persones perquè la "Laura" no sigui l'última de la llista.
Verifica que el test ja no fuciona.
Ja pots modificar el codi per tal que verifiqui que person
és més alt que result
abans de modificar result
:
result = persons[0]
for person in persons:
if person["edat"] > result["edat"]:
result = person
assert result["nom"] == "Laura"
I podem optimitzar el codi començant pel segon element de la llista:
result = persons[0]
for person in persons[1:]:
if person["edat"] > result["edat"]:
result = person
assert result["nom"] == "Laura"
Pytest
El modul pytest
et permet gestionar un conjut de tests.
Ves al teu directori "home":
$ cd
Crea un projecte dolphin
:
$ poetry new dolphin --name app
Obre el projecte amb VS Code:
$ code dolphin
Afegeix una dependència amb pytest
:
$ poetry add pytest --group dev
Activa l'entorn virtual:
$ poetry shell
Creeu un fitxer nou anomenat test_division.py
, que conté una test:
def test_division():
assert division(6,3) == 2
I executa pytest:
$ pytest
> assert division(6,3) == 2
E NameError: name 'division' is not defined
El test que acabem d'escriure ni tan sols es compila. Això és prou fàcil d'arreglar.
Què és el mínim que podem fer perquè es compili, encara que no s'executi?
Necessitem una implementació bàsica de division()
.
De nou farem el mínim de treball possible només per fer que la prova es compila:
def division(a,b):
0
def test_division():
assert division(6,3) == 2
Ara podem executar la prova i veure que falla.
$ pytest
> assert division(6,3) == 2
E assert None == 2
E + where None = division(6, 3)
test_division.py:6: AssertionError
El nostre marc de proves ha executat el petit fragment de codi amb el qual vam començar i ens vam adonar que tot i que esperàvem "2" com a resultat, vam veure "Cap". Tristesa.
No, no. El fracàs és progrés. Ara tenim una mesura concreta del fracàs. Això és millor que només saber vagament que estem fallant. El nostre problema de programació s'ha transformat de "fer una divisió" a "fer que aquesta prova funcioni i després fer que la resta de proves funcionin". Molt més senzill. Un marge molt més petit per a la por. Podem fer que aquesta prova funcioni.
Probablement no us agradarà la solució, però l'objectiu ara mateix no és obtenir la resposta perfecta, l'objectiu és aprovar la prova. Més tard farem el nostre sacrifici a l'altar de la veritat i la bellesa.
Aquí teniu el canvi més petit que podria imaginar que faria que la nostra prova passés:
def division(a,b):
2
I la prova falla. Que passa?
En alguns llenguatges una funció sempre retorna un resultat que és la útlima sentència, però per lo vist en Python no i ho hem d'indicar de manera explícita amb return
.
Gràcies a escriure la prova primer, l'hem après. Ho podem arreglar:
def division(a,b):
return 2
def test_division():
assert division(6,3) == 2
I la nostra prova passa:
$ pytest
test_division.py . [100%]
==================== 1 passed in 0.01s =======================
Però, podem estar segurs que la funció està ben implementada? Escrivim un altre afirmació per provar la funció de divisió perquè qui sap...
def test_division():
assert division(6,3) == 2
assert division(9,3) == 3
I quan provem la nostra funció tenim una petita sorpresa. La prova ha fallat!
$ pytest
E assert 2 == 3
E + where 2 = division(9, 3)
test_division.py:7: AssertionError
Bé, hi ha alguna cosa malament en la nostra funció de divisió. Fixem la funció de divisió:
def division(a,b):
return a / b
Ara tornem a obtenir la barra verda:
============================== 1 passat en 0,01 s =============== ========
Et semblen massa petits aquests passos? Recordeu que el TDD no es tracta de fer petits passos, sinó de poder fer petits passos.
Codificaria el dia a dia amb passos tan petits? No, però quan les coses es fan una mica estranyes, estic content de poder-ho.
Proveu petits passos amb un exemple de la vostra pròpia elecció. Si podeu fer passos massa petits, segur que podeu fer els passos de la mida adequada. Si només feu passos més grans, mai sabreu si els passos més petits són adequats.
Perquè què passarà si fem aquesta afirmació?
assert division(5, 2) == 2.5
Amb Python el quocient retornat per l'operador /
sempre és un float
encara que els operands siguin int
i el resultat pugui ser representat amb un int
.
Però això no és cert en altres idiomes. Per exemple, en Java si dividim dos int
el resultat és un int, en aquest cas el resultat seria 2.
I què passa amb division(2,3)
i la division(4,0)
? Quin resultat esperes?
assert division(2, 3) == ???
assert division(4, 0) == ???
Amb totes aquestes proves implementades, podeu estar segur que si alguna cosa canvia a la funció de divisió ho sabràs.
Sí, sé que l’operador /
no canviarà el seu comportament.
Però les funcions no són tan senzilles, i algunes són molt complexes i depenen d'altres funcions i biblioteques. Podeu estar segur que executant totes les proves que no s'esperava que no hagi canviat.
Aprèn fent tests
A més de provar codi que has fet tu, pots provar codi que han fet altres.
Això és important per apendre com funciona el codi dels altres.
namedtuple()
és un funció disponible a collections
que et permet crear subclasses de tuples amb noms.
D'aquesta manera el codi és més llegible perquè enlloc de un index pots utilitzar un nom per accedir als diferents elements de la tupla mitjnaçant la notació obj.attr
.
Per crear una namedtuple
has de proporcionar dos arguments posicionals a la funció:
-
typename
. El nom de la subclasse. -
field_names
. Una llista dels noms
Crea el fitxer test_tuple.py
:
"""Test the Task data type."""
from collections import namedtuple
Task = namedtuple('Task',['summary','owner','done','id'])
def test_defaults():
"""Using no parameters should invoke defaults."""
t1 = Task()
t2 = Task(None, None, False, None)
assert t1 == t2
TODO Revisar: explicacions més correctes i concises
Suposo que la tupla anomenada es crearà amb valors predeterminats. Perquè no? Python és un llenguatge amigable. Però som desenvolupadors de TDD, així que deixarem que pytest comprove si aquesta suposició és certa:
$ pytest
FAILED test_task.py::test_defaults - TypeError: Task.__new__() missing 4 required positional arguments: 'summary...
============================== 1 failed in 0.02s ===============================
I estàs veient la temida barra vermella.
El problema de Python és que és un llenguatge amigable sense escriptura estàtica, de manera que no pot saber quins tipus fan les nostres tuples i inferir quins valors predeterminats utilitzar.
Després d'aprendre sobre namedtuple, he descobert que pots utilitzar:
Task = namedtuple('Task',['summary','owner','done','id'])
Task.__new__.__defaults__ = (None,None,False,None)
Ara tenim la barra verda, legendaria en cançons i història:
$ pytest
test_task.py . [100%]
============================== 1 passed in 0.01s ===============================
Ara provarem com accedir als membres per nom i no per índex, que és un dels motius principals per utilitzar namedtuples. Afegiu aquesta prova a test_task.py
:
def test_member_access():
"""Check .field functionality of namedtuple."""
t = Task('buy milk', 'brian')
assert t.summary == 'buy milk'
assert t.owner == 'brian'
assert (t.done,t.id) == (False,None)
$ pytest
collected 2 items
test_task.py .. [100%]
============================== 2 passed in 0.01s ===============================
La prova passa, però el més important és que la prova anterior també es prova i encara s'aprova.
Podeu convertir les instàncies de tuple amb nom existents en diccionaris utilitzant ._asdict()
. Aquest mètode retorna un diccionari nou que utilitza els noms dels camps com a claus. Les claus del diccionari resultant estan en el mateix ordre que els camps del namedtuple original
def test_as_dict():
Person = namedtuple("Person", "name age height")
jane = Person("Jane", 25, 1.75)
assert jane._asdict() == ""
Com que les tuples amb nom són immutables, pot semblar contraintuïtiu que l'objecte namedtuple ve amb el mètode ._replace()
, que us permet substituir valors en una tupla amb nom.
La forma en què això funciona és que anou es crea una tupla anomenada.
L'avantatge d'aquest enfocament és que ens permet modificar només valors específics, tot conservant els valors originals
def test_replace():
Person = namedtuple('Person', ['name', 'age', 'location', 'profession'])
mike = Person('Mike', 33, 'Toronto', 'Veterinari')
assert mike.age == 33
newMike = mike._replace(age=44)
assert newMike.age == 44
assert mike.age == 33
assert mike.age != newMike.age
Activitats
1.- Prova que una namedtuple
és pot convertir en un dict
:
def test_asdict():
"""_asdict() should return a dictionary."""
task = Task('do something', 'tokken', True, 21)
# your code
assert dict == expected
def test_asdict():
"""_asdict() should return a dictionary."""
task = Task('do something', 'tokken', True, 21)
dict = task._asdict()
expected = {
'summary':'do something',
'owner': 'tokken',
'done': True,
'id': 21
}
assert dict == expected
2.- Prova que puc canviar els valors d'una namedtuple
amb _replace
:
def test_replace():
"""replace() should change passed in fields."""
task = Task('finish book', 'brian', False)
# your code
assert task == expected
def test_replace():
"""replace() should change passed in fields."""
task = Task('finish book', 'brian', False)
task = task._replace(id=10, done=True)
expected = Task('finish book', 'brian', True, 10)
assert task == expected
Activitats
1.- Seguint la estructura de test que hem vist, crea 3 mètodes de test que provin el funcionament d'aquesta funció.
def adn_count_base(adn, base):
"""Compta el número d'aparicions de la base dintre de la cadena d'adn"""
counter = 0
for x in adn:
if x == base:
counter += 1
return counter
A continuació del mètode principal, poseu aquestes funcions de test.
def test_adn_count_2A():
assert adn_count_base("GATACT","A") == 2
def test_adn_count_1G():
assert adn_count_base("GATACT","G") == 1
def test_adn_count_3T():
assert adn_count_base("ATGGATTAG","T") == 3