Fins ara la seqüència ha estat sempre: escriure codi, després protegir-lo amb tests. El Desenvolupament Guiat per Proves (TDD, Test-Driven Development) inverteix la fletxa: el test s'escriu abans que existeixi el codi que el farà passar. Sona a truc de faquir, però és una disciplina de disseny amb regles precises i beneficis mesurables — i també amb límits que convé conèixer sense fanatisme. La practicaràs amb una peça nova de Papyrus que l'Anna fa setmanes que demana: cupons de descompte per a la campanya d'estiu. Naixerà sencera amb TDD, test a test.
Contingut
- El cicle Vermell-Verd-Refactor
- Les regles del joc
- Kata Papyrus:
aplicar_cupo, iteració a iteració - Refactor amb xarxa de seguretat
- TDD com a eina de disseny
- Beneficis i crítiques honestes
El cicle Vermell-Verd-Refactor
TDD és un cicle de tres fases, curt (minuts, no hores), que es repeteix fins a acabar la funcionalitat:
graph LR
R["VERMELL<br/>Escriu un test que falla<br/>(defineix QUÈ vols)"] --> V["VERD<br/>Escriu el codi MÍNIM<br/>que el fa passar"]
V --> F["REFACTOR<br/>Millora el codi<br/>sense canviar el comportament"]
F --> R
| Fase | Què fas | Què està prohibit |
|---|---|---|
| Vermell | Escrius un test petit per al següent comportament i el veus fallar | Escriure dos tests alhora; saltar-te l'execució "perquè ja sé que falla" |
| Verd | Escrius el mínim — encara que sigui barroer — perquè tota la suite passi | Afegir funcionalitat "ja que hi sóc"; perseguir elegància |
| Refactor | Neteges duplicació i millores noms, amb la suite en verd com a xarxa | Canviar el comportament; refactoritzar amb tests en vermell |
Veure el test fallar primer no és un ritual buit: és la prova que el test prova alguna cosa. Un test que neix en verd pot estar comprovant res (recorda el mètode sense prefix test_ de 09-02, que "passava" perquè no existia).
Les regles del joc
Les tres regles clàssiques (formulades per Robert C. Martin), en versió pràctica:
- No escriguis codi de producció si no és per fer passar un test que està en vermell.
- No escriguis més test del necessari per fallar (un error de compilació/importació ja compta com a fallada).
- No escriguis més codi de producció del necessari per passar el test en vermell.
I una actitud transversal: baby steps — passes de nadó. Cada cicle afegeix un comportament diminut. La temptació de "resoldre-ho tot d'una vegada" és exactament el que TDD ve a domar: passes petites signifiquen que quan alguna cosa es trenca, la causa és a les últimes deu línies.
Kata Papyrus: aplicar_cupo, iteració a iteració
L'encàrrec de l'Anna: "Vull cupons sobre la quantia final d'una venda. El PAPYRUS10 descompta un 10 %, el SOCI5 un 5 %. Un codi invàlid ha de donar un error clar, els cupons no són acumulables, i les quanties sempre a 2 decimals." Una kata és un exercici deliberat per practicar la mecànica; aquesta serà real: el codi acabarà a papyrus/cupons.py.
Iteració 1 — Vermell: el primer test defineix l'API
Encara no existeix ni el mòdul. Comencem escrivint com ens agradaria fer-lo servir:
# tests/test_cupons.py
import pytest
from papyrus.cupons import aplicar_cupo
def test_papyrus10_descompta_deu_per_cent():
# Hamlet x2 amb IVA son 20.70 (dada canonica); -10% = 18.63
assert aplicar_cupo(20.70, "PAPYRUS10") == pytest.approx(18.63)pytest → vermell: ModuleNotFoundError: No module named 'papyrus.cupons'. Perfecte: la regla 2 diu que una fallada d'importació ja és un test en vermell. Sense adonar-nos-en, hem pres tres decisions de disseny: la funció es diu aplicar_cupo, rep (quantia, codi) en aquest ordre i retorna la quantia (no la imprimeix, no muta res).
Iteració 1 — Verd: el codi mínim (encara que faci mal)
Retornar una constant? Sí. És el pas mínim legal, i es diu fake it (fingeix-ho). Sembla absurd, però disciplina una cosa valuosa: el codi només es generalitza quan un test ho exigeix. pytest → verd. Res a refactoritzar encara.
Iteració 2 — Vermell: triangular per forçar la fórmula
Obliguem a generalitzar amb un segon exemple (això s'anomena triangulació):
def test_papyrus10_amb_una_altra_quantia():
assert aplicar_cupo(100.00, "PAPYRUS10") == pytest.approx(90.00)Vermell: 18.63 != 90.0. La constant ja no cola; ara sí, la fórmula és el mínim:
Verd (els dos tests passen — la suite sencera s'executa sempre).
Iteració 3 — Vermell: el segon cupó
def test_soci5_descompta_cinc_per_cent():
# Una Odissea sense carnet (13.00) amb SOCI5 → 12.35:
# el mateix preu que pagaria un soci, perque ×0.95×1.04 commuta
assert aplicar_cupo(13.00, "SOCI5") == pytest.approx(12.35)Vermell: ha retornat 11.70 (ha aplicat el 10 %). Mínim per al verd: distingir codis.
def aplicar_cupo(quantia: float, codi: str) -> float:
if codi == "SOCI5":
return round(quantia * 0.95, 2)
return round(quantia * 0.90, 2)Verd. Fa mala olor (qualsevol codi desconegut descompta un 10 %?), però aquesta olor és el següent test, no un arranjament furtiu: la regla 3 prohibeix avançar-se.
Iteració 4 — Vermell: codi invàlid → error clar
L'error mereix la seva pròpia excepció a la jerarquia de M7:
# A papyrus/errors.py, al costat de les seves germanes:
class CupoInvalidError(ErrorPapyrus):
def __init__(self, codi: str):
self.codi = codi
super().__init__(f"Cupo no valid: {codi!r}")from papyrus.errors import CupoInvalidError
def test_codi_invalid_llanca_error():
with pytest.raises(CupoInvalidError, match="ESTIU99"):
aplicar_cupo(20.70, "ESTIU99")Vermell (ha retornat 18.63 en lloc de fallar). Verd:
from papyrus.errors import CupoInvalidError
def aplicar_cupo(quantia: float, codi: str) -> float:
if codi == "PAPYRUS10":
return round(quantia * 0.90, 2)
if codi == "SOCI5":
return round(quantia * 0.95, 2)
raise CupoInvalidError(codi)Iteració 5 — Vermell: no acumulables
Què vol dir "no acumulables" en aquesta API? La nostra decisió de disseny: la funció accepta un codi; qualsevol intent de combinar ("PAPYRUS10+SOCI5") no és un codi vàlid i s'ha de rebutjar:
def test_els_cupons_no_sacumulen():
with pytest.raises(CupoInvalidError):
aplicar_cupo(20.70, "PAPYRUS10+SOCI5")pytest → ja està en verd! El codi actual rebutja qualsevol codi desconegut. Test inútil? No: els requisits de l'Anna ara estan escrits. Si demà algú "millora" la funció per trossejar codis per +, aquest test es posarà vermell i defensarà la regla de negoci. Un test que neix verd exigeix una comprovació extra: trenca'l a mà un segon (canvia la quantia esperada) per verificar que s'executa, i restaura'l.
Iteració 6 — Vermell: l'arrodoniment com a contracte
def test_arrodoneix_a_dos_decimals():
# 9.99 - 10% = 8.991 → ha de sortir 8.99, mai 8.991
assert aplicar_cupo(9.99, "PAPYRUS10") == 8.99Verd a la primera (el round ja hi era), i de nou ho fixem com a contracte: si un refactor perd l'arrodoniment, hi ha xarxa.
Refactor amb xarxa de seguretat
Sis tests en verd. Ara — i només ara — embellim. La cascada d'if duplica el patró "percentatge + round"; un diccionari ho expressa millor (M4 al rescat):
# papyrus/cupons.py — versio final despres del refactor
from papyrus.errors import CupoInvalidError
CUPONS: dict[str, float] = {
"PAPYRUS10": 0.10,
"SOCI5": 0.05,
}
def aplicar_cupo(quantia: float, codi: str) -> float:
"""Aplica un cupo a la quantia final i retorna el resultat a 2 decimals.
Llança CupoInvalidError si el codi no existeix. No acumulable.
"""
if codi not in CUPONS:
raise CupoInvalidError(codi)
return round(quantia * (1 - CUPONS[codi]), 2)pytest → els sis tests continuen en verd. Aquesta és la sensació que defineix TDD: hem reescrit la funció sencera sense por, perquè la suite garanteix que el comportament no ha canviat. Afegir el cupó de Nadal serà una línia al diccionari... precedida, és clar, del seu test.
TDD com a eina de disseny
Fixa't en el que va passar a la iteració 1: en escriure primer la crida, vam dissenyar l'API des del punt de vista de qui la fa servir. La signatura aplicar_cupo(quantia: float, codi: str) -> float va sortir de l'ús, no d'un diagrama. TDD empeny sistemàticament cap a funcions petites, sense efectes ocults i fàcils de cridar — perquè el que és difícil de provar es nota immediatament al test. Si el test necessita preparar mig món, el disseny està cridant que la funció depèn de massa coses (el mateix senyal que vam anticipar amb unittest.mock a 09-03). El test vermell és, abans que verificació, una especificació executable.
Beneficis i crítiques honestes
| TDD compensa quan... | TDD fa nosa quan... |
|---|---|
| La lògica de negoci té regles clares (preus, cupons, estoc) | Estàs explorant i encara no saps què vols (prototips, notebooks) |
| El cost d'un bug és alt (diners, dades) | El codi és rebutjable o trivial (un script d'una tarda) |
| Mantindràs el codi durant anys | L'API depèn d'un sistema extern que encara no entens |
| Vols refactoritzar sense por | Provar requereix infraestructura pesada que encara no domines (GUI, maquinari) |
Beneficis reals: cobertura que neix amb el codi (no com a tasca ajornable), disseny orientat a l'ús, documentació executable i refactor sense por. Crítiques legítimes: els tests són codi que també es manté (dupliquen el cost de canviar una regla de negoci a propòsit); TDD mal entès produeix tests acoblats a la implementació que es trenquen amb cada refactor (prova el comportament, no les interioritats); i la disciplina estricta pot ser lenta en fases exploratòries. La postura madura no és religiosa: TDD per a la lògica de negoci amb regles clares — com els preus de Papyrus —, tests-després per a l'exploratori, i sempre, sempre, un test de regressió quan cacis un bug (ho veuràs a la propera lliçó).
Errors Comuns i Consells
- No executar el test en vermell abans de codificar. És l'error número u: un test mal escrit pot néixer verd i no protegir res. Vermell primer, sempre vist amb els teus ulls.
- Escriure cinc tests de cop i després el codi. Això és "test-first per lots", no TDD: perds el diàleg test↔disseny i els baby steps. Un test, un verd, cada vegada.
- Saltar-se el refactor. Verd no és la meta, és la meitat. Si acumules verds sense netejar, el codi degenera i acabaràs culpant TDD del teu deute tècnic.
- Provar la implementació en lloc del comportament. Un test que comprova "fa servir un diccionari anomenat CUPONS" es trenca amb cada refactor legítim. Els nostres tests només miren entrades i sortides: per això el refactor de la cascada d'
ifal diccionari no va tocar ni una línia de test. - Perseguir el 100 % de cobertura com a fi. La cobertura mesura quines línies s'executen, no quins contractes es verifiquen. Sis tests amb intenció valen més que vint de tautològics.
- Consell: mantén el cicle curt. Si portes vint minuts en vermell, el pas era massa gran: esborra, parteix el problema i torna amb un test més petit.
Exercicis
Exercici 1
L'Anna afegeix el cupó de campanya "ESTIU15" (−15 %). Fes-ho amb TDD estricte: escriu el test (fes servir aplicar_cupo(20.00, "ESTIU15"), calcula tu l'esperat), mira'l fallar, fes el canvi mínim i confirma el verd. Quantes línies de producció has tocat?
Exercici 2
Nou requisit: una quantia negativa no té sentit i ha de llançar ValueError (no CupoInvalidError: el problema és la quantia, no el cupó — coherent amb el criteri de vendre a M7). Cicle complet: test vermell → mínim → verd.
Exercici 3
La Júlia va teclejar "papyrus10" en minúscules i va rebre un error. L'Anna decideix que els cupons han de ser insensibles a majúscules. Escriu el test vermell, fes-lo passar amb el canvi mínim i respon: s'ha posat vermell algun test anterior amb el teu canvi? Per què és important executar la suite sencera a cada verd?
Solucions
Exercici 1 — esperat: 20.00 × 0.85 = 17.00.
def test_estiu15_descompta_quinze_per_cent():
assert aplicar_cupo(20.00, "ESTIU15") == pytest.approx(17.00)Vermell (CupoInvalidError). Verd amb una línia en producció: afegir "ESTIU15": 0.15, al diccionari CUPONS. Aquest és el dividend del refactor de la iteració final: les dades i la lògica van quedar separades.
Exercici 2
def test_quantia_negativa_llanca_valueerror():
with pytest.raises(ValueError):
aplicar_cupo(-5.00, "PAPYRUS10")Vermell (retorna -4.5). Mínim per al verd, al principi de la funció:
Exercici 3
def test_cupo_en_minuscules_es_valid():
assert aplicar_cupo(100.00, "papyrus10") == pytest.approx(90.00)Vermell. Canvi mínim: normalitzar a l'entrada — codi = codi.upper() com a primera línia (o codi.strip().upper() si vols tolerar espais; això demanaria el seu propi test). Cap test anterior es trenca: "PAPYRUS10".upper() continua sent "PAPYRUS10" i "PAPYRUS10+SOCI5" continua sense existir. Però només ho saps perquè has executat la suite sencera: cada verd és una afirmació sobre tots els contractes alhora, no només sobre l'últim. Aquest hàbit — suite completa a cada cicle — és el que converteix sis tests en una xarxa i no en sis cordes soltes.
Conclusió
Papyrus té una funcionalitat nova — papyrus/cupons.py amb aplicar_cupo(quantia, codi) i el seu CupoInvalidError a la jerarquia de M7 — que no ha existit ni un minut sense tests: va néixer vermell-verd-refactor, amb l'API dissenyada des de l'ús, triangulació per forçar la generalització i un refactor final sense por gràcies a la xarxa de sis tests. També tens el mapa honest: TDD brilla en lògica de negoci amb regles clares i fa nosa en l'exploració; fes-lo servir com a eina, no com a religió. Però siguem realistes: per moltes proves que escriguis, els bugs continuaran arribant — en codi heretat, en el cas límit que ningú no va imaginar, en producció un divendres. La lliçó següent tracta d'això: què fer quan alguna cosa ja està trencada. Reproduir, aïllar, corregir i — tancant el cercle amb aquesta lliçó — protegir el que s'ha corregit amb una prova de regressió. És hora d'aprendre a depurar amb mètode.
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
