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

  1. El cost d'un bug: el matí que l'Anna va trencar el preu de soci
  2. Què és una prova: el patró AAA (preparar-actuar-comprovar)
  3. assert a pèl: la primera eina i els seus límits
  4. El precursor artesanal: if __name__ == "__main__"
  5. Tipus de prova i la piràmide
  6. Què provar primer: casos feliços, límits i errors esperats
  7. 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:

  1. El cas feliç (happy path): la funció usada com s'espera, amb dades normals.
  2. Els límits: les vores on nien els bugs — zero, u, exactament-el-màxim, buit.
  3. 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.csv real (a 09-03 veuràs tmp_path per a això).
  • Llegible: un test és documentació executable. test_vendre_sense_estoc_llanca_error_i_no_muta_cataleg explica 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_final arrodoneix a 2 decimals, però 0.1 + 0.2 == 0.3 és False (ho vas veure a M1). A 09-02 i 09-03 coneixeràs assertAlmostEqual i pytest.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 assert per a validacions de producció. Recorda: python -O els elimina. Per validar entrades d'usuari, llança excepcions (ValueError); reserva assert per 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats