Papyrus Online funciona — o això sembla quan el proves a mà. Però "ho vaig provar a mà dimarts" no és verificació: és una anècdota. Aquesta lliçó converteix els criteris d'acceptació de 12-01 en una suite de tests executable, la que et permet tocar qualsevol peça del sistema i saber en vint segons si has trencat res. Aplicarem la piràmide de M9 al projecte complet — què es prova a cada nivell i amb quina eina —, escriurem el test d'integració que recorre tot el flux (vendre → persistir → rellegir → quadrar caixa), provarem la interfície amb el seu client de proves, i aprendrem a depurar a través de les capes quan un test digui que no. Al final: la checklist de qualitat que decideix si el projecte està llest per lliurar-se.
Contingut
- La piràmide aplicada a Papyrus Online
- Dades de prova vs dades reals: les fixtures canòniques
- La suite mínima exigible (criteris → tests)
- El test d'integració del flux complet
- Provar la interfície: test client de Flask / Client de Django
- Regressions: cada bug deixa el seu test
- Depurar el sistema integrat
- Checklist final de qualitat
La piràmide aplicada a Papyrus Online
A 09-01 la piràmide era teoria; ara té noms i cognoms. Cada nivell prova una cosa diferent, amb una eina diferent, i en la proporció correcta (molts a baix, pocs a dalt):
| Nivell | Què es prova AQUÍ | Eina | Quants | Velocitat |
|---|---|---|---|---|
| Unitari (domini) | preu_final, aplicar_cupo, hi_ha_estoc, construcció de Llibre/Soci |
pytest pur, sense disc ni xarxa | 10-15 | mil·lisegons |
| Servei | ServeiVendes complet: les tres fases, cada error, cada combinació soci/cupó |
pytest + repositoris sobre tmp_path |
8-12 | ràpids |
| Integració | El flux sencer: vendre → persistir → rellegir → el tancament de caixa quadra | pytest + tmp_path |
2-3 | moderats |
| Interfície | Cada ruta retorna el seu codi i el seu JSON/HTML | test client (Flask) / Client (Django) |
5-8 | moderats |
La regla de repartiment: si un comportament es pot provar en un nivell inferior, es prova allà. L'arrodoniment de 12.35 es verifica al nivell unitari, no llançant peticions HTTP; l'endpoint només verifica que tradueix bé (JSON correcte, codi correcte), perquè el fons ja està provat més avall. Provar l'arrodoniment per HTTP funciona, però és lent, fràgil i, quan falla, no et diu on és el bug.
Dades de prova vs dades reals
Regla absoluta: els tests no toquen mai dades/. Un test que escriu al teu catàleg real és una bomba: et corromp les dades o, pitjor, passa o falla segons el que vas vendre ahir. Tots els tests treballen sobre tmp_path (M9) amb les fixtures canòniques — els quatre llibres de sempre, precisament perquè fa onze mòduls que en coneixes els números de memòria:
# tests/conftest.py
import json
import pytest
from papyrus.repositoris import (RegistreVendes, RepositoriCataleg,
RepositoriSocis)
from papyrus.serveis import ServeiVendes
CATALEG = [
{"titol": "L'Odissea", "autor": "Homer", "preu": 12.50, "estoc": 4},
{"titol": "Hamlet", "autor": "Shakespeare", "preu": 9.95, "estoc": 6},
{"titol": "El Quixot", "autor": "Cervantes", "preu": 15.90, "estoc": 8},
{"titol": "Faust", "autor": "Goethe", "preu": 21.00, "estoc": 10},
]
SOCIS = [{"codi": "LLUIS-001", "nom": "Lluis", "alta": "2025-03-12"},
{"codi": "MARTA-002", "nom": "Marta", "alta": "2025-06-30"},
{"codi": "PAU-003", "nom": "Pau", "alta": "2026-01-15"}]
@pytest.fixture
def rutes(tmp_path):
"""Un dades/ de mentida, acabat de crear per a CADA test (aillament)."""
(tmp_path / "cataleg.json").write_text(
json.dumps(CATALEG, ensure_ascii=False), encoding="utf-8")
(tmp_path / "socis.json").write_text(
json.dumps(SOCIS, ensure_ascii=False), encoding="utf-8")
return tmp_path
@pytest.fixture
def servei(rutes):
"""Un ServeiVendes muntat sobre el dades/ de mentida."""
return ServeiVendes(RepositoriCataleg(rutes / "cataleg.json"),
RepositoriSocis(rutes / "socis.json"),
RegistreVendes(rutes / "vendes.csv"))Aquí cobra sentit la decisió de disseny de 12-02: com que el servei rep els repositoris ja construïts, els tests li injecten repositoris de tmp_path sense tocar ni una línia del codi de producció. Si el teu servei hagués obert "dades/cataleg.json" a pèl, aquesta lliçó seria impossible. El codi fàcil de provar i el codi ben dissenyat són el mateix codi.
La suite mínima exigible
Els criteris d'acceptació de 12-01, un a un, convertits en tests amb nom. Aquesta taula és el teu RNF1: la suite està completa quan cada fila existeix i passa:
| Test | Requisit que verifica |
|---|---|
test_preu_final_soci_i_no_soci (parametritzat: les 8 quanties canòniques) |
RF2 |
test_alta_llibre_duplicat_falla |
RF1 |
test_esborrar_llibre_i_consultar_lo_llanca_no_trobat |
RF1 |
test_vendre_a_soci_valid_aplica_descompte (12.35 amb LLUIS-001) |
RF2, RF3 |
test_vendre_amb_soci_desconegut_llanca_soci_invalid |
RF2 |
test_vendre_sense_estoc_llanca_error_i_no_modifica_res |
RF3 |
test_vendre_amb_cupo_valid_i_invalid (18.67 / CupoInvalidError) |
RF3 |
test_venda_persisteix_i_es_rellegeix (el d'integració, a sota) |
RF4 |
test_guardar_es_atomic_no_deixa_json_corrupte |
RF4 |
test_api_rutes_retornen_codis_correctes (o vistes Django) |
RF5 |
test_informe_reprodueix_numeros_canonics (520 u, dissabte 130) |
RF6 |
test_venda_rebutjada_deixa_warning_al_log (amb caplog) |
RF7 |
Amb les parametritzacions, això suma uns 25 tests. No és un número màgic: és la traducció literal del contracte de 12-01. Si demà afegeixes un requisit, la seva fila apareix aquí abans que el seu codi — això era TDD (09-03) i aquest projecte és el millor lloc per practicar-lo.
El test d'integració del flux complet
El test més valuós del projecte: recorre les quatre capes de dades (servei → disc → rellegir → agregar) i comprova que la història completa quadra. Complet i comentat:
# tests/test_integracio.py
from datetime import date
from papyrus.repositoris import (RegistreVendes, RepositoriCataleg,
RepositoriSocis)
from papyrus.serveis import ServeiVendes
def test_flux_complet_vendre_persistir_rellegir_quadrar(rutes):
# --- 1. VENDRE: soci + cupo, dues vendes ---
servei = ServeiVendes(RepositoriCataleg(rutes / "cataleg.json"),
RepositoriSocis(rutes / "socis.json"),
RegistreVendes(rutes / "vendes.csv"))
v1 = servei.vendre("Faust", 1, codi_soci="LLUIS-001", cupo="PAPYRUS10")
v2 = servei.vendre("Hamlet", 1) # la Julia, sense soci
assert v1.quantia == 18.67 # 21.00 ×1.04 ×0.95 ×0.90
assert v2.quantia == 10.35 # 9.95 ×1.04
# --- 2. PERSISTIR i RELLEGIR: una instancia NOVA, que nomes veu el disc ---
# Si l'estoc es correcte aqui, es perque guardar() i carregar() funcionen
# de veritat, no perque l'objecte vell ho recordes en memoria.
cataleg_rellegit = RepositoriCataleg(rutes / "cataleg.json").carregar()
assert cataleg_rellegit["Faust"].estoc == 9 # era 10
assert cataleg_rellegit["Hamlet"].estoc == 5 # era 6
# --- 3. QUADRAR: el tancament de caixa surt del CSV, no de la memoria ---
servei_nou = ServeiVendes(RepositoriCataleg(rutes / "cataleg.json"),
RepositoriSocis(rutes / "socis.json"),
RegistreVendes(rutes / "vendes.csv"))
avui = date.today().isoformat()
assert servei_nou.tancament_de_caixa(avui) == 29.02 # 18.67 + 10.35El detall que el fa un test d'integració de veritat és al pas 2: la instància nova. Si reutilitzes el servei original, el test pot passar encara que guardar() estigui trencada, perquè l'objecte recorda l'estoc en memòria. Rellegir des del disc és el que demostra RF4.
Provar la interfície
Itinerari A — test client de Flask (10-03). Per injectar el tmp_path necessitaràs el patró app factory: una funció crear_app(servei) en lloc de muntar el servei global en importar. És un refactor de deu minuts que torna a demostrar la regla: provar millora el disseny.
# tests/test_app.py
import pytest
from app import crear_app
@pytest.fixture
def client(servei): # reutilitza la fixture de conftest
app = crear_app(servei)
app.config["TESTING"] = True
return app.test_client()
def test_venda_sense_estoc_retorna_409(client):
resposta = client.post("/api/vendes",
json={"titol": "L'Odissea", "unitats": 99})
assert resposta.status_code == 409
assert "error" in resposta.get_json() # el cos explica el rebuig
def test_venda_valida_retorna_201_amb_quantia(client):
resposta = client.post("/api/vendes",
json={"titol": "Faust", "unitats": 1,
"soci": "LLUIS-001", "cupo": "PAPYRUS10"})
assert resposta.status_code == 201
assert resposta.get_json()["quantia"] == 18.67Itinerari B — Client de Django (10-05), on TestCase et dona una base de dades neta per test (el seu equivalent del tmp_path):
# cataleg/tests.py
from django.test import TestCase
from cataleg.models import Llibre
class TestVenda(TestCase):
def setUp(self):
Llibre.objects.create(titol="L'Odissea", autor="Homer",
preu=12.50, estoc=4)
def test_venda_sense_estoc_mostra_error_al_form(self):
resposta = self.client.post("/vendre/",
{"titol": "L'Odissea", "unitats": 99})
self.assertEqual(resposta.status_code, 200) # el form es torna a mostrar
self.assertContains(resposta, "estoc") # ...amb l'error visibleFixa't en què verifiquen aquests tests: la traducció, no el negoci. Que el 409 sigui 409, que el JSON porti la quantia, que el form torni a mostrar l'error. El càlcul del 18.67 ja va quedar demostrat dos nivells més avall.
Regressions: cada bug deixa el seu test
Durant 12-03 vas trobar bugs (tots en trobem). La regla de 09-05, ara com a disciplina de projecte: cap bug no es tanca sense el seu test. El flux: bug detectat → escrius el test que el reprodueix (falla, vermell) → arregles → el test passa (verd) → el test es queda a la suite per sempre. Aquest test val més que deu tests inventats: protegeix un punt on el teu sistema va demostrar que sabia trencar-se. Anota a DECISIONS.md cada regressió amb una línia ("tancament_de_caixa sumava strings — test_tancament_converteix_quanties"): a 12-05, aquesta llista serà or per a la teva retrospectiva.
Depurar el sistema integrat
Quan un test (o l'ús real) falla al sistema complet, el bug pot ser a qualsevol de les quatre capes. Tres tècniques, per ordre:
- Llegir el traceback A TRAVÉS de les capes. Un traceback del sistema integrat és llarg: línies de Flask/Django, línies teves d'
app.py, deserveis.py, derepositoris.py. Recorre'l de baix a dalt i localitza l'última línia teva: aquesta és la capa on ha esclatat. Però compte: on esclata no sempre és on es va trencar — unKeyErroral servei pot ser un JSON mal guardat pel repositori fa tres vendes. El traceback et dona la capa de l'explosió; la causa pot viure una capa més avall. - El log com a testimoni entre capes.
papyrus.logregistra què va fer cada capa i en quin ordre (RF7 ja no és un requisit burocràtic: és la teva caixa negra). L'endpoint ha retornat 500? Mira el log: si l'última línia és l'INFOd'una venda, la fallada va ser després de vendre — al guardat o a la serialització de la resposta. Acabes de descartar dues capes sense obrir el depurador. pdbal punt d'integració.breakpoint()just on una capa entrega a la següent (la crida de l'endpoint al servei, la del servei al repositori) i inspecciona el paquet que creua la frontera:p dades,p type(fila["unitats"]). Com deia 12-03: la majoria dels bugs d'integració són "creia que em passaves X i era Y" — i la frontera és on es veu.
Checklist final de qualitat
Abans de declarar el projecte "lliurable", passa-li aquesta llista. No és burocràcia: és la diferència entre creure que està bé i saber-ho.
- [ ] RF1: alta duplicada rebutjada; baixa + consulta → no trobat. Test en verd.
- [ ] RF2: les 8 quanties canòniques exactes (parametritzat); soci fals rebutjat.
- [ ] RF3: venda sense estoc no modifica res (verifica l'estoc després de l'error!); 18.67 amb soci+cupó; cupó fals rebutjat.
- [ ] RF4: el test d'integració passa; matar el procés durant un guardat no corromp el JSON.
- [ ] RF5: totes les rutes/vistes amb el seu test de codi d'estat.
- [ ] RF6: informe sobre
vendes_2026.csv→ 520 unitats, dissabte 130, PNG generat. - [ ] RF7:
caplog(o el fitxer) confirma elWARNINGa cada rebuig. - [ ] RNF1:
pytest→ tot verd, ~25 tests, cap no toca eldades/real. - [ ] RNF2: zero funcions públiques del paquet sense type hints.
- [ ] RNF3: zero
except Exceptiongenèrics sense justificació escrita. - [ ] RNF4: el README existeix i una altra persona podria arrencar el projecte amb ell (es verifica a 12-05).
Errors Comuns i Consells
- Provar el negoci per HTTP. Si
test_api_...verifica arrodoniments, tens la piràmide invertida: lenta, fràgil i amb diagnòstics pobres. L'endpoint prova la traducció; el domini, el fons. - Tests que comparteixen estat. El símptoma clàssic: passen en solitari, fallen en suite (o segons l'ordre). Causa: escriuen al mateix lloc. La fixture
rutescrea un món nou per test; usa-la sempre. - Comparar floats a pèl.
assert quantia == 18.67funciona aquí perquè arrodonim a 2 decimals en un únic punt (12-03). Si un assert de quanties et falla per 0.0000001, no "arreglis el test ambpytest.approx": tens un arrodoniment de més o de menys al codi.approxés per a matemàtiques contínues, no per a diners arrodonits. - Esborrar el test d'un bug arreglat ("ja no falla, sobra"). És exactament a l'inrevés: és l'únic test amb una fallada real demostrada al seu historial.
- Depurar afegint
printpertot arreu. Ja tens tres eines millors i ordenades: traceback (quina capa?), log (què va passar abans?), pdb (què creua la frontera?). Elsprintsolts acaben oblidats al codi — el log és unprintamb contracte.
Exercicis
- Fita de projecte — la suite completa. Implementa les 12 files de la suite mínima (amb les seves parametritzacions). Verificació:
pytest -ven verd, i el nom de cada test diu quin RF cobreix sense obrir el fitxer. - Fita de projecte — el test del guardat atòmic. Escriu
test_guardar_es_atomic: provoca una fallada a mitges deguardar(pista: passa-li un catàleg amb un objecte no serialitzable, que farà esclatarjson.dump) i comprova que el fitxer original continua intacte i carregable. Aquest test justifica tot el disseny de l'os.replacede 12-03. - Fita de projecte — la teva regressió. Tria el bug més dolorós que vas trobar a 12-03, escriu el seu test de regressió amb un nom que expliqui la història, i afegeix la línia corresponent a
DECISIONS.md.
Solucions
- Autocomprovació en lloc de solució: executa
pytest --collect-onlyi compara la llista amb la taula de la suite mínima; cada fila ha de tenir almenys un test recollit. Sitest_informe_...triga uns quants segons, està bé: és el preu de pandas + PNG, i per això n'hi ha un, no vint. - Orientativa:
cataleg_trencat = dict(cataleg); cataleg_trencat["X"] = object()iwith pytest.raises(TypeError): repo.guardar(cataleg_trencat); després,repo.carregar()ha de retornar el catàleg original sense error. Si el teuguardarescrivia directament sobreself._ruta, aquest test t'ho dirà amb unJSONDecodeError— i acabes d'entendre, amb una fallada controlada, per a què existia el fitxer temporal. - Sense solució única. Criteri de qualitat del nom: algú que no va viure el bug ha d'entendre què protegeix —
test_tancament_de_caixa_converteix_quanties_a_floatsí;test_bug_arreglat_2no.
Conclusió
Papyrus Online ja no és codi que sembla funcionar: és un sistema verificat. La piràmide ha deixat de ser un dibuix — domini amb tests unitaris a mil·lisegons, servei sobre tmp_path, un test d'integració que recorre vendre → persistir → rellegir → quadrar, i una interfície la traducció HTTP de la qual està demostrada amb el seu client de proves. Els criteris de 12-01 són ara 25 tests amb nom, cada bug del desenvolupament ha deixat la seva regressió, i saps depurar a través de les capes amb traceback, log i pdb a les fronteres. La checklist final està marcada: el projecte està acabat. Però acabat no és el mateix que lliurat: falta que una altra persona pugui instal·lar-lo, entendre'l i veure'l funcionar — i falta que tu puguis explicar-lo. Documentació, demo, informe final i la mirada enrere sobre tot el camí: això és l'última lliçó del curs.
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
