El mòdul 8 va acabar amb una pregunta incòmoda: quan demà canviïs una línia de Papyrus — un descompte nou, un camp més a vendes.csv — com garanteixes que tot continua funcionant? Els type hints de mypy verifiquen que els tipus encaixen, però no que preu_final() retorni 12.35 € i no 12.36 €. Comprovar-ho a mà cada vegada no escala. La resposta de l'enginyeria de programari és escriure codi que comprova el codi: proves. En aquesta lliçó entendràs per què són imprescindibles, quina anatomia té un test i què val la pena provar primer — abans de tocar cap framework.
Contingut
- El cost d'un bug: el matí que l'Anna va trencar el preu de soci
- Què és una prova: el patró AAA (preparar-actuar-comprovar)
asserta pèl: la primera eina i els seus límits- El precursor artesanal:
if __name__ == "__main__" - Tipus de prova i la piràmide
- Què provar primer: casos feliços, límits i errors esperats
- Qualitats d'un bon test
El cost d'un bug: el matí que l'Anna va trencar el preu de soci
Un dimarts, l'Anna decideix "endreçar una mica" el mètode preu_final() de models.py. L'original ho calculava tot en una expressió; ella ho separa en passos i, de passada, arrodoneix el resultat intermedi "perquè quedi més net":
# Versio original (correcta): arrodoneix UNA vegada, al final
def preu_final(self, soci: bool = False) -> float:
base = self.preu * (1 - DESCOMPTE_SOCI) if soci else self.preu
return round(base * (1 + IVA_LLIBRES), 2)
# Versio "endreçada" de l'Anna (incorrecta): arrodoneix DUES vegades
def preu_final(self, soci: bool = False) -> float:
base = self.preu
if soci:
base = round(base * (1 - DESCOMPTE_SOCI), 2) # ← arrodoniment intermedi
return round(base * (1 + IVA_LLIBRES), 2)Sembla inofensiu. Però per a L'Odissea (12.50 €), el preu de soci correcte és 12.50 × 0.95 × 1.04 = 12.35 €. Amb l'arrodoniment intermedi: 12.50 × 0.95 = 11.875 → round → 11.88 → × 1.04 = 12.3552 → 12.36 €. Un cèntim de més. L'Anna no se n'adona: ho va provar mentalment amb Hamlet, on el doble arrodoniment casualment dóna el mateix resultat. Una setmana després, la sòcia Júlia reclama, i el tancament_de_caixa() fa set dies que no quadra.
La lliçó no és "l'Anna és descuidada". És que el cost d'un bug creix brutalment com més tard es detecta:
| Moment de detecció | Cost típic | A Papyrus |
|---|---|---|
| Mentre escrius el codi | Segons | L'Anna veu el test en vermell i corregeix abans de desar |
| Abans d'integrar (suite de tests) | Minuts | pytest falla a la seva màquina; ningú més ho pateix |
| En ús real (producció) | Hores o dies + confiança | La Júlia reclama, cal revisar 7 tancaments de caixa i tornar cèntims |
Un test que hagués fixat per escrit «el preu de soci de L'Odissea és 12.35» hauria detectat el bug en segons. Això és exactament el que aprendrem a escriure.
Què és una prova: el patró AAA
Una prova (test) és un fragment de codi que executa un altre fragment de codi amb entrades conegudes i comprova que el resultat coincideix amb l'esperat. Gairebé tots els tests del món segueixen la mateixa estructura en tres passos, anomenada patró AAA per les sigles en anglès (Arrange, Act, Assert):
| Pas | En anglès | Què fas | Exemple a Papyrus |
|---|---|---|---|
| 1. Preparar | Arrange | Muntes l'escenari: objectes, dades, fitxers | Crear Llibre("L'Odissea", 12.50, 4) |
| 2. Actuar | Act | Executes **l'**operació que vols provar | Cridar preu_final(soci=True) |
| 3. Comprovar | Assert | Verifiques que el resultat és l'esperat | Ha retornat 12.35? |
En codi, amb el que ja saps del curs:
from papyrus.models import Llibre
# 1. Preparar: un llibre amb les dades canoniques del cataleg
llibre = Llibre("L'Odissea", 12.50, 4)
# 2. Actuar: l'operacio sota prova
preu = llibre.preu_final(soci=True)
# 3. Comprovar: assert falla amb AssertionError si la condicio es False
assert preu == 12.35, f"esperava 12.35, he obtingut {preu}"Fixa't en tres detalls:
- Cada test comprova una cosa. Si aquest test falla, saps exactament què s'ha trencat: el preu de soci de L'Odissea.
- El valor esperat està escrit a mà (
12.35), no calculat amb la mateixa fórmula que el codi provat. Si copiessis la fórmula, el test tindria el mateix bug que el codi i no fallaria mai. - El missatge de l'
assert(la f-string després de la coma) et diu què ha passat sense obrir el depurador.
assert a pèl: la primera eina i els seus límits
La sentència assert la coneixes del __post_init__ de Llibre i de validacions puntuals: si la condició és falsa, llança AssertionError. Només amb això ja pots escriure un script de comprovació:
# comprova_papyrus.py — la nostra "suite" artesanal
from papyrus.models import Llibre
from papyrus.magatzem import vendre, cercar_llibre
from papyrus.errors import EstocInsuficientError
cataleg = {"Hamlet": Llibre("Hamlet", 9.95, 6)}
# Cas feliç: 2 Hamlet amb IVA son 20.70 € i l'estoc baixa a 4
quantia = vendre(cataleg, "Hamlet", 2)
assert quantia == 20.70, f"quantia incorrecta: {quantia}"
assert cataleg["Hamlet"].estoc == 4, "l'estoc no s'ha descomptat"
# Error esperat: demanar mes estoc del disponible ha de llançar l'excepcio
try:
vendre(cataleg, "Hamlet", 99)
assert False, "hauria d'haver llançat EstocInsuficientError"
except EstocInsuficientError:
pass # correcte: l'excepcio esperada ha arribat
# Titol inexistent: cercar_llibre retorna None, no rebenta
assert cercar_llibre(cataleg, "Rayuela") is None
print("Totes les comprovacions han passat.")Això ja és provar, i és infinitament millor que res. Però té límits seriosos:
| Límit | Conseqüència |
|---|---|
| S'atura al primer fallada | Si fallen 5 coses, només en veus 1; corregeixes, executes, veus la següent... |
| No hi ha informe | Ningú et diu "12 han passat, 2 han fallat, són aquestes"; només un traceback o un print final |
python -O elimina els assert |
Amb el flag d'optimització, la teva "suite" no comprova res i no avisa |
| L'estat es comparteix | El segon bloc hereta el catàleg mutat pel primer: els tests es contaminen entre ells |
| Executar és manual | T'has de recordar de llançar-ho, i de llançar-ho tot |
| Comprovar excepcions és feixuc | El ball try/except/assert False és propens a errors |
Els frameworks de testing (unittest a 09-02, pytest a 09-03) existeixen precisament per resoldre aquesta llista: descobreixen els tests, els executen tots, els aïllen entre ells i produeixen un informe llegible.
El precursor artesanal: if __name__ == "__main__"
Al mòdul 3, quan vam crear papyrus_utils.py, vam afegir al final del fitxer un bloc if __name__ == "__main__": amb una "mini-demo": unes crides d'exemple que només s'executaven en llançar el mòdul directament, no en importar-lo. Allò era, sense saber-ho, l'ancestre d'un test: codi que exercita el mòdul amb dades conegudes.
# Al final de papyrus_utils.py, alla pel modul 3:
if __name__ == "__main__":
# mini-demo / mini-prova manual
print(formatar_preu(12.5)) # imprimeix "12.50 €"?La diferència clau: aquella demo imprimia i un humà jutjava amb els ulls si el resultat era correcte. Un test afirma (assert) i és el mateix programa qui jutja. Aquest salt — de "mirar la sortida" a "codificar l'expectativa" — és tota l'essència del testing. Tota la resta (frameworks, fixtures, TDD) és maquinària al voltant d'aquesta idea.
Tipus de prova i la piràmide
No totes les proves operen al mateix nivell. La classificació clàssica en distingeix tres:
| Tipus | Què comprova | Exemple a Papyrus | Velocitat | Quantitat recomanada |
|---|---|---|---|---|
| Unitària | Una peça petita i aïllada (una funció, un mètode) | preu_final(soci=True) retorna 12.35 |
Mil·lisegons | Moltes |
| Integració | Que diverses peces col·laboren bé juntes | tancament_de_caixa() llegeix un vendes.csv real de prova i suma bé |
Dècimes de segon | Algunes |
| End-to-end (E2E) | El sistema complet, com el faria servir una persona | Simular la jornada sencera: carregar catàleg, vendre, reposar, tancar caixa | Segons o més | Poques |
La proporció recomanada es dibuixa com una piràmide: base ampla de tests unitaris (ràpids i barats), capa mitjana d'integració, i una punta fina d'E2E (lents, fràgils, cars de mantenir):
graph TD
E2E["End-to-end: poques<br/>(lentes, fràgils, cares)"] --> INT["Integració: algunes<br/>(mòduls que col·laboren)"]
INT --> UNIT["Unitàries: moltes<br/>(ràpides, aïllades, barates)"]
En aquest mòdul ens centrarem en la base de la piràmide — les unitàries — amb alguna incursió en integració (provar tancament_de_caixa contra un CSV de prova). Les proves E2E d'una aplicació web les veuràs de passada quan Papyrus arribi a la web al mòdul 10.
Què provar primer: casos feliços, límits i errors esperats
Davant d'una funció, quins casos mereixen test? Hi ha tres famílies, per ordre de prioritat:
- El cas feliç (happy path): la funció usada com s'espera, amb dades normals.
- Els límits: les vores on nien els bugs — zero, u, exactament-el-màxim, buit.
- Els errors esperats: les entrades invàlides han de fallar tal com està documentat (les excepcions del mòdul 7 són part del contracte!).
Apliquem-ho a les dues funcions estrella de Papyrus. Per a Llibre.preu_final:
| Família | Cas | Esperat |
|---|---|---|
| Feliç | L'Odissea sense ser soci | 12.50 × 1.04 = 13.00 |
| Feliç | L'Odissea sent soci | 12.35 |
| Límit | Arrodoniment a 2 decimals | Hamlet soci → 9.83 (no 9.8306) |
| Error | Llibre("X", -5.0) |
__post_init__ llança ValueError |
I per a vendre(cataleg, titol, unitats), que té un contracte més ric (el vam blindar al mòdul 7):
| Família | Cas | Esperat |
|---|---|---|
| Feliç | Vendre 2 Hamlet | Retorna 20.70 i l'estoc passa de 6 a 4 |
| Límit | Vendre exactament tot l'estoc (4 Odissees) | Funciona; l'estoc queda a 0 |
| Error | Vendre 5 Odissees (només n'hi ha 4) | EstocInsuficientError i el catàleg queda intacte |
| Error | Títol inexistent | LlibreNoTrobatError |
| Error | unitats=0 o negatives |
ValueError |
| Error | unitats="dos" |
TypeError |
Observa la fila d'EstocInsuficientError: el test no només comprova que l'excepció salta, sinó que no hi ha hagut efectes secundaris (l'estoc no ha canviat). Recorda de 08-04 que vendre muta el catàleg al final precisament per garantir això — i un test és la manera que aquesta garantia no es perdi en un refactor futur.
Qualitats d'un bon test
No qualsevol codi amb assert és un bon test. Els bons comparteixen quatre qualitats:
- Ràpid: la suite sencera ha de córrer en segons. Si triga, deixaràs d'executar-la, i un test que no s'executa no protegeix res.
- Aïllat: cada test munta el seu propi escenari i no depèn d'altres tests ni de l'ordre d'execució. L'script artesanal d'abans violava això: el segon bloc heretava el catàleg mutat pel primer.
- Repetible: el mateix resultat avui, demà i al portàtil d'en Lluís. Res de dependre de l'hora actual, de la xarxa o del
dades/cataleg.csvreal (a 09-03 veuràstmp_pathper a això). - Llegible: un test és documentació executable.
test_vendre_sense_estoc_llanca_error_i_no_muta_catalegexplica el contracte millor que un paràgraf de comentaris.
Errors Comuns i Consells
- Provar només el cas feliç. Els bugs viuen a les vores: estoc 0, llistes buides, títols inexistents. Si els teus tests només proven el normal, protegeixen poc.
- Calcular el valor esperat amb la mateixa fórmula que el codi provat.
assert preu == round(12.50 * 0.95 * 1.04, 2)repetiria el bug si la fórmula està malament. Escriu el número literal:12.35. - Comparar floats amb
==alegrement. Aquí funciona perquèpreu_finalarrodoneix a 2 decimals, però0.1 + 0.2 == 0.3ésFalse(ho vas veure a M1). A 09-02 i 09-03 coneixeràsassertAlmostEqualipytest.approx. - Tests que depenen de dades reals. Si el teu test llegeix
dades/cataleg.csv, fallarà el dia que l'Anna afegeixi un llibre. Crea les dades dins del test. - Confiar en
assertper a validacions de producció. Recorda:python -Oels elimina. Per validar entrades d'usuari, llança excepcions (ValueError); reservaassertper a tests i invariants internes. - Consell: quan trobis un bug a mà, escriu primer el test que el reprodueix i després corregeix-lo. Aquest test de regressió evitarà que el bug torni (ho sistematitzarem a 09-05).
Exercicis
Exercici 1
Escriu, amb assert a pèl i seguint el patró AAA (comenta cada pas), tres comprovacions per a Llibre.preu_final: (a) El Quixot sense soci, (b) El Quixot amb soci, (c) Faust amb soci. Calcula a mà els valors esperats amb les constants canòniques (DESCOMPTE_SOCI=0.05, IVA_LLIBRES=0.04, arrodoniment a 2 decimals).
Exercici 2
Sense escriure codi: dissenya en una taula (família / cas / resultat esperat) els casos de prova de reposar(cataleg, titol, unitats) -> None, seguint el model de la taula de vendre. Inclou com a mínim un cas feliç, un límit i dos errors esperats.
Exercici 3
Classifica cada escenari com a prova unitària, d'integració o end-to-end: (a) hi_ha_estoc() retorna False quan estoc == 0; (b) tancament_de_caixa() suma correctament un vendes.csv de tres files creat per a la prova; (c) simular una jornada completa de Papyrus: carregar catàleg des de fitxer, vendre 3 llibres, reposar-ne 2 i tancar caixa comprovant el total; (d) EstocInsuficientError guarda titol, demanat i disponible als seus atributs.
Solucions
Exercici 1
from papyrus.models import Llibre
# (a) El Quixot sense soci: 15.90 × 1.04 = 16.536 → 16.54
llibre = Llibre("El Quixot", 15.90, 8) # Preparar
preu = llibre.preu_final() # Actuar
assert preu == 16.54, f"esperava 16.54, he obtingut {preu}" # Comprovar
# (b) El Quixot amb soci: 15.90 × 0.95 × 1.04 = 15.7092 → 15.71
preu = llibre.preu_final(soci=True)
assert preu == 15.71, f"esperava 15.71, he obtingut {preu}"
# (c) Faust amb soci: 21.00 × 0.95 × 1.04 = 20.748 → 20.75
faust = Llibre("Faust", 21.00, 10)
preu = faust.preu_final(soci=True)
assert preu == 20.75, f"esperava 20.75, he obtingut {preu}"Fixa't com cada valor esperat és un literal calculat a mà, no la fórmula.
Exercici 2
| Família | Cas | Resultat esperat |
|---|---|---|
| Feliç | Reposar 5 Hamlet (estoc 6) | Retorna None; l'estoc passa a 11 |
| Límit | Reposar 1 unitat | Estoc 6 → 7 (el mínim vàlid funciona) |
| Error | Títol inexistent | LlibreNoTrobatError i catàleg intacte |
| Error | unitats=0 o negatives |
ValueError; l'estoc no canvia |
| Error | unitats=2.5 o "tres" |
TypeError; l'estoc no canvia |
Exercici 3: (a) unitària — un mètode aïllat; (b) integració — col·laboren el lector de CSV, el parseig i la suma contra un fitxer real (de prova); (c) end-to-end — recorre el sistema complet com ho faria l'Anna; (d) unitària — comprova una classe d'excepció aïllada.
Conclusió
Ja tens el mapa: un test és codi que fixa per escrit el que el programa ha de fer — preparar, actuar, comprovar — i el bug de l'Anna va demostrar que detectar un error en segons costa infinitament menys que detectar-lo quan la Júlia reclama. Saps que l'assert a pèl funciona però no escala (s'atura a la primera fallada, no informa, no aïlla), que la piràmide mana moltes proves unitàries i poques E2E, i que cal cobrir casos feliços, límits i errors esperats amb tests ràpids, aïllats, repetibles i llegibles. El if __name__ == "__main__" del mòdul 3 va ser el nostre precursor artesanal; ara toca l'eina industrial. A la propera lliçó, la biblioteca estàndard ens dóna el primer framework complet: unittest, amb classes de test, asercions especialitzades (inclosa una per als floats dels nostres preus) i un executor que descobreix i corre tota la suite amb una sola ordre.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
