Proves
- La piràmide de les proves
- Tipus de prova segons l’objectiu
- Escriure una bona prova
- Test doubles
- Pràctiques guiades per proves
- Saber si les proves serveixen
- Proves intermitents
- Les proves dins del cicle de desenvolupament
- Qui fa les proves
- El paper de les proves amb assistents i agents
- Referències
Tenir codi llest per a producció requereix fer proves. Com que no les podem repetir totes a mà cada vegada que canviem alguna cosa, sorgeix la necessitat d’automatitzar-les. Un procés que va de la mà de les pràctiques de desenvolupament àgil i el lliurament continu.
Aquesta pàgina recull els conceptes que no depenen del llenguatge, amb exemples en Java per concretar-los. Les eines es tracten a les pàgines de cada tecnologia: JUnit per a Java, Vitest i Testing Library per a JavaScript.
La piràmide de les proves
La piràmide de les proves reparteix la suite en tres capes segons què abasta cada prova, de les més nombroses i barates a les més escasses i cares:
- Proves unitàries: validació dels blocs més atòmics del software, les funcions.
- Proves d’integració: validació que diferents peces del software funcionen juntes. Per exemple, comunicació amb la base de dades, la xarxa o el sistema d’arxius.
- Proves end to end (E2E): validació des del punt de vista de l’usuari, tractant el software com una caixa negra.
En el món real, aquesta categorització no és estricta, hi ha proves que poden estar al mig d’aquestes capes.
| Unitàries | Integració | End-to-end | |
|---|---|---|---|
| Objectiu principal | Funcions individuals | Integració de funcions | Funcionalitat d’usuari |
| Quantitat | Nombroses | Una mica freqüent | Escassa |
| Velocitat | Molt alta | Mitjana | Lenta |
| Freqüència d’execució | Alta, quan es desenvolupa una funció | Regular, quan es desenvolupa una funcionalitat | Quan s’acaba una funcionalitat |
| Feedback | Entrada i sortida per a la funció | Comportament problemàtic | Funcionalitat incorrecta |
| Cost | Baix: petita, fàcil d’actualitzar, executar i entendre | Moderat | Costós |
| Coneixement de l’aplicació | Acoblat al codi | Codi, bases de dades, xarxa, arxius | Inconscient |
| Beneficis | Feedback ràpid durant el desenvolupament, evitar regressions, documentació | Ús de llibreries de tercers, comprova efectes secundaris | Funcionament correcte de l’usuari |
Cada nivell atrapa una classe de fallada diferent, i el criteri per repartir-hi l’esforç es desenvolupa a Nivells de test.
Tipus de prova segons l’objectiu
La piràmide classifica les proves per abast: quina part del sistema toquen. Hi ha un segon eix independent, el de per què les executes, i és el que dona nom a la major part del vocabulari que sentiràs. Una mateixa prova pot ser alhora unitària per abast i de regressió per objectiu.
Proves de regressió
Una regressió és un comportament que abans funcionava i ara no. És la fallada més cara del desenvolupament, perquè ningú la busca: el canvi que la provoca sol estar en una altra part del codi i la persona que el fa no té cap motiu per sospitar.
Una prova de regressió és qualsevol prova que es conserva i es torna a executar per detectar-ho. Això vol dir que no és un tipus de prova diferent de les altres, sinó el paper que fa tota la suite a partir del moment en què la puges al repositori: la vas escriure per verificar una funcionalitat nova i, a partir de l’endemà, la seva feina és avisar-te si algú la trenca.
La pràctica que se’n deriva és senzilla i val la pena convertir-la en costum: cada vegada que arregles un bug, escriu primer una prova que el reprodueixi. Ha de fallar abans de la correcció i passar després. Guanyes dues coses: la garantia que has arreglat el que creies, i la garantia que aquell bug concret no tornarà mai sense que la suite ho digui.
Perquè aquest paper funcioni, la suite s’ha d’executar sencera i automàticament, no quan algú se’n recorda. És la feina de la integració contínua.
Proves d’acceptació
Una prova d’acceptació respon una pregunta diferent de la resta: no si el codi fa el que el programador volia, sinó si el sistema fa el que es va acordar amb qui el va demanar. Per això el criteri no el fixa l’equip de desenvolupament tot sol, i s’escriu amb el vocabulari del domini (“una comanda ja pagada no es pot cancel·lar”), no amb el de la implementació.
Sovint coincideixen amb el nivell end-to-end de la piràmide, però no és el mateix: l’abast diu per on passa la prova, i l’acceptació diu qui n’ha decidit el resultat esperat. Quan aquests criteris s’acorden abans de programar i es fan executables, tenim BDD i ATDD.
Són també la porta d’entrada a la prova d’acceptació d’usuari (UAT), la validació manual que fa el client abans de donar per bona una entrega.
Altres objectius habituals
La resta de noms que sentiràs es distingeixen per la pregunta que responen, no per on passen:
| Tipus | Què pregunta |
|---|---|
| Proves de fum (smoke) | Val la pena provar res més? Un subconjunt mínim i ràpid que comprova que l’aplicació arrenca i respon. S’executa just després de desplegar |
| Proves de caracterització | Què fa realment aquest codi heretat, abans que hi posi les mans? |
| Proves de rendiment i càrrega | Aguanta el volum esperat i respon prou ràpid? No verifiquen correcció, sinó límits |
| Proves de seguretat | Es comporta bé davant d’entrades malicioses, no només davant de les previstes? |
| Proves de contracte | Les dues bandes d’una frontera continuen entenent-se? Es tracten a Fronteres de servei |
Escriure una bona prova
Les proves són tan importants com el codi: es llegeixen, es mantenen i envelleixen igual, i per tant se’ls apliquen els mateixos criteris de qualitat. D’aquí surten les pistes per fer-les mantenibles:
- Prova només una funcionalitat per prova. Que sigui curta farà que sigui més clara.
- Escriure el codi amb funcions petites ajuda a fer proves més granulars.
- La claredat és més important que no repetir-se.
Que una prova sigui fàcil o difícil d’escriure diu molt del codi que prova. Ho tractem a Disseny per a la testabilitat i a Llegir els senyals de les proves.
Arrange, act i assert
L’estructura recomanada per a qualsevol prova, sigui del nivell que sigui, és una seqüència de tres passos:
- Arrange: prepara les entrades i els objectius de la prova.
- Act: invoca el comportament, cridant una funció, interactuant amb un API, una pàgina web, etc.
- Assert: comprova que el resultat és l’esperat.
També es coneix com a given, when, then, que és el mateix patró amb el vocabulari del BDD. Els tres blocs es distingeixen visualment amb una línia en blanc:
@Test
void retirar_ambSaldoSuficient_deixaElSaldoRestant() {
Compte compte = new Compte(100); // arrange
compte.retirar(30); // act
assertEquals(70, compte.saldo()); // assert
}
Una asserció (assertion) és la comprovació que fa fallar la prova quan la condició no es compleix. Una prova pot acabar de tres maneres: passa, falla perquè una asserció no s’ha complert, o dona error perquè ha saltat una excepció que ningú esperava.
Com anomenar una prova
El nom d’una prova és el que llegeixes quan falla al pipeline, sovint sense el codi al davant. Compara aquests dos:
testRetirar()
retirar_ambSaldoInsuficient_llencaExcepcio()
El primer no diu res. El segon segueix el patró unitat, condició, resultat esperat, i llegit en veu alta ja és una frase: “retirar, amb saldo insuficient, llença excepció”. Quan la prova falla, el nom sol ser suficient per saber què s’ha trencat.
Quan el framework permet posar el nom com a text lliure, com JUnit amb @DisplayName, escriu directament la frase sencera:
@Test
@DisplayName("no permet retirar més diners dels que hi ha")
void retirar_ambSaldoInsuficient_llencaExcepcio() { ... }
El conjunt de noms de proves d’una classe acaba sent la llista de promeses que fa. Si la llista no s’entén sense obrir el codi, o hi ha una única prova anomenada testTot(), la documentació que hauria de donar la suite s’ha perdut.
Test doubles
Els test doubles són versions simplificades, o directament falses, de les dades o funcions reals, i serveixen per reduir la complexitat de la prova.
Per comprovar que Compte rebutja una retirada que el servei antifrau marca com a sospitosa, no cal parlar amb el servei antifrau real: n’hi ha prou amb un objecte que sempre respongui “sospitosa”. Aquest objecte és un test double, i n’hi ha de diversos graus, de menys a més específic:
- Dummy: objectes que es passen però no s’utilitzen.
- Fake: implementacions que funcionen, però no serveixen per a producció.
- Stubs: proporcionen respostes enllaunades a certes preguntes, no responent a res que no hi hagi a la prova.
- Spies: són stubs que guarden informació de com van ser cridats.
- Mocks: estan programats amb expectatives, i per tant poden comprovar si la crida no s’espera, o falta alguna crida, llençant excepcions. Per tant, abans d’utilitzar-los cal indicar quines són les expectatives. Molts desenvolupadors utilitzen “mocks” per parlar de forma general dels dobles.
Millors pràctiques:
- Si es pot, evitar els dobles i treballar amb implementacions reals.
- De vegades no és possible, per diferents raons: volem provar certs escenaris (d’èxit o error) però no els podem generar, o bé és molt lent, o no volem treballar amb les dades reals. En general, escenaris on el codi té side effects.
- Llavors ens cal simular comportaments de certa dependència o servei extern.
- Entre els possibles dobles, és convenient seleccionar el menys específic (millor un dummy que un mock).
- Un cop seleccionat el doble, millor implementar-lo que utilitzar un framework.
El lloc on s’injecta un doble és un seam: el punt on una implementació es pot canviar per una altra sense tocar el codi que la fa servir.
Pràctiques guiades per proves
Hi ha diverses maneres de fer que les proves dirigeixin el desenvolupament, i el TDD només és la més coneguda. Es diferencien en qui escriu el criteri, a quin nivell i en quin format:
| Pràctica | Qui escriu la prova | Nivell | Què fixa |
|---|---|---|---|
| TDD | La persona que programa | Unitari | El disseny d’una unitat, pas a pas |
| BDD i ATDD | L’equip amb qui demana la funcionalitat | Acceptació | Què vol dir que la funcionalitat està feta |
| Proves de caracterització | Qui ha de tocar codi heretat | El que calgui | El comportament actual, abans de canviar-lo |
| Proves basades en propietats | La persona que programa | Unitari | Una regla que s’ha de complir per a qualsevol entrada |
No són excloents. Un mateix projecte pot tenir escenaris d’acceptació acordats amb el client, TDD per al codi nou i proves de caracterització al mòdul antic que ningú vol tocar.
Test-driven development
El TDD (desenvolupament guiat per proves) inverteix l’ordre habitual: primer s’escriu la prova unitària i després el codi que la fa passar. El procediment és aquest:
- Afegir un nou test
- Executar tots els tests. La nova prova ha de fallar.
- Escriure el codi més senzill que permeti passar el test.
- Totes les proves han de funcionar novament.
- Fer refactoring quan calgui, utilitzant els tests per assegurar-se que la funcionalitat es preserva.
La passa 2 no és un tràmit. Una prova que passa abans d’escriure el codi no verifica res, i escriure-la després de veure el resultat porta a copiar-hi el comportament que ja té el codi en lloc del que hauria de tenir.
Vegem-ho amb el Compte. La primera volta del cicle comença per la prova de la retirada correcta, que és exactament la de l’apartat arrange, act i assert. Encara no existeix retirar(), de manera que la prova ni tan sols compila: aquest és el vermell de la passa 2. El codi més senzill que la posa verda és aquest:
public void retirar(int quantitat) {
saldo -= quantitat;
}
És clarament incomplet, però és correcte per a l’única prova que existeix, i el TDD no deixa escriure res que cap prova hagi demanat. El cas que falta entra a la volta següent:
@Test
void retirar_ambSaldoInsuficient_llencaExcepcio() {
Compte compte = new Compte(100);
assertThrows(SaldoInsuficientException.class, () -> compte.retirar(150));
assertEquals(100, compte.saldo());
}
Falla, perquè el codi actual deixa el saldo a -50 sense queixar-se. Ara sí que la comprovació està justificada:
public void retirar(int quantitat) {
if (quantitat > saldo) {
throw new SaldoInsuficientException();
}
saldo -= quantitat;
}
Fixa’t en el que ha passat. La validació no s’ha escrit perquè algú se n’ha recordat, sinó perquè una prova l’ha exigida, i el codi no ha crescut més enllà del que les proves demanaven.
El TDD té un efecte secundari que sovint importa més que les proves resultants: com que escrius primer com es fa servir el codi, acabes amb interfícies més fàcils d’utilitzar. Si la prova és incòmoda d’escriure, el disseny encara no és bo.
També té els seus inconvenients. Demana saber què vols abans de programar, cosa que no passa quan encara estàs explorant el problema, i llavors el camí honest és un spike sense proves que després es llença. A més, les proves queden acoblades a la granularitat de les unitats que has anat creant: un refactor que en fusioni o en parteixi obliga a reescriure-les. Per això el TDD estricte és minoritari a la indústria, tot i que la regla que el sosté no ho és.
BDD i ATDD
El TDD respon “aquest mètode fa el que jo esperava”. No respon si el que jo esperava era el que calia. Aquest segon problema és el que ataquen el BDD (behaviour-driven development) i l’ATDD (acceptance test-driven development): l’exemple del comportament esperat s’acorda abans de programar amb qui demana la funcionalitat, i s’escriu amb el seu vocabulari, no amb el del codi.
El format habitual és l’escenari given-when-then, escrit en Gherkin.
El format Gherkin
Gherkin és un llenguatge de text pla amb molt poques paraules clau, dissenyat perquè el pugui llegir algú que no programa. Els escenaris es guarden en fitxers .feature:
- Funcionalitat (Feature): agrupa els escenaris relacionats i n’explica el context.
- Escenari (Scenario): un cas concret. És la unitat que s’executa.
- Donat, Quan, Aleshores (Given, When, Then): les tres passes de l’escenari, que són l’arrange, act i assert amb un altre vocabulari.
- I, Però (And, But): afegeixen una línia continuant la paraula clau anterior.
- Rerefons (Background): passes comunes que s’executen abans de cada escenari del fitxer.
- Esquema de l’escenari (Scenario Outline) amb Exemples (Examples): el mateix escenari repetit amb una taula de dades.
# language: ca
Funcionalitat: Retirada de diners
Rerefons:
Donat un compte amb 100 euros
Escenari: retirada per sobre del saldo
Quan s'intenta retirar 150 euros
Aleshores l'operació es rebutja
I el saldo continua sent 100 euros
Esquema de l'escenari: qualsevol import per sobre del saldo es rebutja
Quan s'intenta retirar <retirada> euros
Aleshores l'operació es rebutja
Exemples:
| retirada |
| 101 |
| 500 |
| 1000 |
La capçalera # language: ca és la que activa les paraules clau en català. Sense ella, Gherkin les espera en anglès, i cada llengua té les seves: el then català és Aleshores o Cal, no Llavors.
Els step definitions
L’escenari no és documentació, és codi executable. Cada línia s’enganxa a un step definition, un mètode amb una anotació que conté el mateix text i amb els valors que varien marcats com a paràmetres:
public class RetiradaSteps {
private Compte compte;
private Exception error;
@Given("un compte amb {int} euros")
public void unCompteAmb(int saldo) {
compte = new Compte(saldo);
}
@When("s'intenta retirar {int} euros")
public void sIntentaRetirar(int quantitat) {
try {
compte.retirar(quantitat);
} catch (SaldoInsuficientException e) {
error = e;
}
}
@Then("l'operació es rebutja")
public void operacioRebutjada() {
assertNotNull(error);
}
@Then("el saldo continua sent {int} euros")
public void elSaldoContinuaSent(int saldo) {
assertEquals(saldo, compte.saldo());
}
}
Quatre anotacions cobreixen les sis línies de passos dels dos escenaris, perquè l’esquema reutilitza els passos que ja existien. Aquest és el guany de la capa: escrius el step definition un cop i després els escenaris es componen amb text.
Val la pena fixar-se en tres coses més. Els passos es comuniquen entre ells pels camps de la classe, perquè cadascun és un mètode independent. L’anotació no ha de coincidir amb la paraula clau de l’escenari: Cucumber ignora el Donat o el Quan i només busca el text que ve després, de manera que l’escenari pot estar en català i les anotacions en anglès. I la línia que comença amb I continua la paraula clau anterior, per això aquí és un @Then.
Aquesta capa de traducció és tota la diferència entre Gherkin i una prova JUnit normal. A favor, un sol artefacte fa de requisit, prova i documentació, que és el que s’anomena especificació amb exemples (specification by example) i que no es pot desactualitzar sense que la suite es posi vermella; la conversa amb qui demana la funcionalitat passa abans del codi, i sovint val més que el fitxer resultant; i els escenaris, escrits en vocabulari de domini, sobreviuen a un refactor intern. En contra, hi ha una indirecció més per saber què comprova un escenari, la glue és codi que s’ha de mantenir i que tendeix a duplicar-se perquè els passos són text lliure, l’estat viu en camps compartits i les proves solen ser lentes perquè travessen el sistema sencer.
El fracàs més habitual no és cap d’aquests: és que el client no llegeixi mai els escenaris. Llavors l’equip paga tota la sintaxi i n’és l’única audiència, i Gherkin no aporta res que no aporti JUnit amb bons noms de prova. Per això el BDD té sentit per a les regles de negoci que algú de fora de l’equip ha de validar, no per a cada mètode.
Proves de caracterització
En codi heretat sense proves no pots començar per escriure el criteri correcte, perquè encara no saps què fa el codi ni saps si el que fa és el que hauria de fer. Una prova de caracterització (characterization test), terme de Michael Feathers, dona la volta al plantejament: escriu proves que capturin el comportament actual, sigui correcte o no.
El procediment és escriure una prova amb una asserció qualsevol, executar-la, veure què retorna de debò el codi i posar aquest valor a l’asserció. La prova no diu que el codi sigui correcte, diu que fa el que feia ahir. Amb aquesta xarxa ja pots refactoritzar sabent que, si trenques alguna cosa, te n’assabentaràs, i llavors sí, corregir el que estigui malament.
És l’excepció deliberada a la regla del TDD: aquí el criteri sí que surt del codi que jutja, i s’accepta perquè l’objectiu no és validar el comportament sinó congelar-lo mentre el toques.
El guany és que et deixen tocar codi que no goses tocar, que sovint és l’únic camí per començar. El preu és que congelen els bugs igual que la resta, i que les assercions queden plenes de valors màgics que no expliquen cap intenció. Són una bastida, no la suite definitiva: a mesura que entens el mòdul, s’han d’anar substituint per proves que diguin què ha de passar.
Proves basades en propietats
Una prova d’exemple comprova un cas concret: retirar 30 de 100 deixa 70. Una prova basada en propietats (property-based testing) comprova una regla que s’ha de complir per a qualsevol entrada, i deixa que la llibreria en generi centenars, incloent-hi els casos límit que tu no hauries pensat (el zero, el negatiu, el desbordament, la cadena buida).
@Property
void elSaldoMaiEsNegatiu(@ForAll @Positive int saldo, @ForAll @Positive int retirada) {
Compte compte = new Compte(saldo);
try { compte.retirar(retirada); } catch (SaldoInsuficientException e) { }
assertTrue(compte.saldo() >= 0);
}
Quan una propietat falla, l’eina redueix automàticament l’entrada al cas més petit que encara falla, i sovint et dona directament el bug. A Java es fa amb jqwik.
A favor, troben casos límit que ningú havia previst, que és exactament el que una prova d’exemple no pot fer. En contra, són més lentes, un error no sempre és fàcil d’interpretar, i la dificultat real no és l’eina sinó trobar quines propietats ha de complir el teu domini: invariants (el saldo mai no és negatiu), operacions inverses (desserialitzar el que has serialitzat torna l’original) o resultats independents de l’ordre. Per això complementen les proves d’exemple, no les substitueixen.
Quan s’escriuen les proves
Cap d’aquestes pràctiques no obliga a escriure sempre la prova abans. El que no es pot fer, en cap d’elles, és deduir el criteri del codi que ha de jutjar. Les opcions habituals (proves després del codi, spike i reescriptura, verificació en producció) es comparen a El criteri de correcció.
Saber si les proves serveixen
Tenir moltes proves verdes no vol dir tenir el codi verificat. Hi ha dues eines per mesurar-ho, i mesuren coses molt diferents.
Cobertura
La cobertura de codi (code coverage) mesura quin percentatge del codi s’ha executat mentre corrien les proves. Es presenta per línies, per branques o per condicions, i les eines et marquen les línies que cap prova no ha arribat a tocar.
Serveix per detectar forats: un mòdul amb 0% de cobertura segur que no està provat. Però la mètrica és només un senyal negatiu. Aquesta prova dona 100% de cobertura sobre retirar i no comprova absolutament res:
@Test
void retirar() {
new Compte(100).retirar(30);
}
La línia s’ha executat, i per a l’eina això és cobertura. Ningú ha comprovat el resultat. Per això perseguir un percentatge com a objectiu (el clàssic “cal arribar al 80%”) produeix proves escrites per pujar el número, no per verificar comportament.
Testing de mutacions
El testing de mutacions (mutation testing) respon la pregunta que la cobertura no respon: si trenquem el codi, la suite se n’adona?
L’eina agafa el codi, hi introdueix un canvi petit i deliberat (un mutant), i executa les proves:
- Si alguna prova falla, el mutant està mort. La suite ha detectat el canvi.
- Si totes les proves passen, el mutant ha sobreviscut. Aquell comportament no està verificat per ningú.
- Un mutant equivalent és un canvi que no altera el comportament observable, i per tant és impossible de matar. Cal descartar-los a mà.
El mutation score és la proporció de mutants morts sobre els generats, un cop descomptats els equivalents. És una mètrica molt més exigent que la cobertura: la prova retirar() de l’apartat anterior té 100% de cobertura i mata zero mutants.
Els canvis que introdueix l’eina són sistemàtics, i cadascun correspon a un error típic de programació:
| Operador de mutació | Exemple |
|---|---|
| Frontera d’una condició | a < b passa a a <= b |
| Negació d’una condició | a == b passa a a != b |
| Operació aritmètica | a + b passa a a - b |
| Valor de retorn | return x passa a return null o return 0 |
| Eliminació d’una crida | s’esborra la invocació d’un mètode void |
| Increment | i++ passa a i-- |
El concepte no depèn del llenguatge, però sí el cost. El que canvia entre ecosistemes és on s’injecta la mutació: PIT muta el bytecode de la JVM ja compilat, que és ràpid; les eines de llenguatges interpretats muten l’arbre sintàctic o el codi font, i les de llenguatges compilats sense capa intermèdia necessiten recompilar cada mutant.
| Llenguatge | Eina |
|---|---|
| Java i JVM | PIT (pitest) |
| C#, JavaScript, TypeScript, Scala | Stryker |
| Python | mutmut, cosmic-ray |
| PHP | Infection |
| Go | go-mutesting, gremlins |
| Rust | cargo-mutants |
En qualsevol cas és molt més lent que executar la suite un cop, perquè la suite s’executa una vegada per mutant. El costum és aplicar-lo als mòduls crítics i deixar-lo per a la integració nocturna, no per a cada commit.
Proves intermitents
Una prova intermitent (flaky) és la que passa o falla amb el mateix codi, i una suite que en té és pitjor que no tenir-ne cap: ensenya l’equip a tornar a llançar el pipeline en lloc d’investigar el vermell. Quan això passa, la suite ja no protegeix res, perquè el dia que falli de veritat ningú s’ho creurà.
Les causes habituals:
- Dependència del temps: esperes fixes (
sleep(500)) que són suficients a la teva màquina i no al servidor de CI, o proves que comparen contra la data actual. - Dependència de l’ordre: la prova A deixa estat que la prova B necessita. Es detecta executant la suite en ordre aleatori.
- Estat compartit: una base de dades, un fitxer o una variable estàtica que no es neteja entre proves.
- Concurrència: la prova assumeix un ordre d’execució entre fils que no està garantit.
- Dependències externes: una API real, la xarxa o un servei de tercers que un dia respon lent.
Davant d’una prova intermitent, arregla-la o esborra-la. Marcar-la perquè s’ignori és acceptable només com a mesura temporal i amb data de caducitat, perquè una prova ignorada envelleix fins que ja no té sentit recuperar-la.
Les proves dins del cicle de desenvolupament
Les proves no s’executen totes al mateix moment ni al mateix lloc. Cada etapa admet un cost diferent i, per tant, un conjunt de proves diferent. La regla que ordena la taula és que com més tard es detecta un error, més car és d’arreglar.
| Moment | Què s’hi executa | Temps acceptable | Qui veu la fallada |
|---|---|---|---|
| Mentre programes, a l’IDE | Les proves unitàries del que estàs tocant | Segons | Tu |
| Abans del commit, amb un git hook | Linters i les proves unitàries ràpides | Menys d’un minut | Tu |
| A cada push, a la integració contínua | La suite sencera, incloent-hi integració | Minuts | L’equip |
| Abans de desplegar, a staging | End-to-end i acceptació | Desenes de minuts | L’equip |
| Just després de desplegar | Proves de fum i health checks | Segons | Qui està de guàrdia |
| En producció | Monitoratge, desplegaments graduals, feature flags | Continu | Els usuaris, si arriba tard |
Les dues primeres files són el teu bucle personal i han de ser ràpides, perquè les executes desenes de vegades al dia. La tercera és la que protegeix la branca principal, i és l’única que no es pot saltar ningú.
Res d’això funciona si les proves s’executen a mà. Una suite que cal recordar de llançar acaba no llançant-se, i el pipeline que la converteix en obligatòria es descriu a Automatització del lliurament.
Qui fa les proves
La qualitat no és responsabilitat d’un departament, és de l’equip que construeix. Aquesta frase sona a eslògan, però té una conseqüència molt concreta: qui escriu una funcionalitat escriu també les proves que la verifiquen, i no la dona per acabada fins que passen.
El model contrari va ser l’habitual durant dècades. El desenvolupament acabava, i llavors el codi passava a un equip de proves separat que buscava errors i els retornava. Falla per dues raons. La primera és de calendari: els errors es troben setmanes després d’introduir-los, quan són molt més cars d’arreglar. La segona és de responsabilitat: si algú altre comprova la teva feina, deixes de comprovar-la tu. D’aquí ve l’expressió llançar el codi per sobre del mur, i el moviment que la corregeix, el shift left: moure la verificació tan a prop de l’origen com es pugui.
Que la responsabilitat sigui compartida no vol dir que tothom faci el mateix:
| Rol | Què hi aporta |
|---|---|
| Qui programa | Les proves unitàries i d’integració del que escriu, i deixar la branca principal verda |
| QA o tester | Dissenyar els casos que ningú ha pensat, provar de manera exploratòria i defensar la perspectiva de l’usuari |
| SDET (enginyeria de proves) | Construir i mantenir la infraestructura: entorns, dades de prova, framework end-to-end, pipeline |
| Producte, o qui demana la funcionalitat | Fixar els criteris d’acceptació i validar que els escenaris diuen el que han de dir |
| El client | La prova d’acceptació d’usuari abans de donar per bona una entrega |
| Qui està de guàrdia | El monitoratge i els health checks, que són la verificació que continua després del desplegament |
En equips petits, una mateixa persona fa diversos d’aquests papers. El que no funciona és que ningú els faci.
Els tres amics
La pràctica que materialitza tot això al BDD es diu three amigos: abans de programar una funcionalitat, es reuneixen tres perspectives durant una estona curta.
- Negoci: què ha de passar i per què.
- Desenvolupament: què implica i quins casos límit apareixen.
- Proves: com podria fallar i com sabrem que funciona.
El resultat de la reunió són els escenaris acordats. El guany real no és el document, són les preguntes que surten allà i que, si no, s’haurien descobert a mitja implementació.
Proves exploratòries
Una suite automàtica només troba el que algú va anticipar quan la va escriure. Per definició, no pot trobar el cas que ningú va imaginar.
Aquesta feina és humana i té nom, proves exploratòries: algú fa servir el sistema amb intenció de trencar-lo, sense guió previ, seguint el que va descobrint. Quan hi troba alguna cosa, aquell cas es converteix en una prova de regressió i ja no cal tornar-lo a buscar mai més a mà. És la divisió de feina raonable entre persones i màquines: les persones descobreixen, la suite recorda.
El paper de les proves amb assistents i agents
Els assistents de codi no han canviat què és una bona prova. Han canviat què costa cada cosa, i això reordena les prioritats d’aquesta pàgina.
La verificació passa a ser el coll d’ampolla
Escriure codi era la part cara i verificar-lo la barata. Amb un agent que genera dues-centes línies en trenta segons, la relació s’inverteix: el que limita la velocitat ja no és produir el canvi, sinó saber si el canvi és correcte.
La conseqüència pràctica és que una suite lenta o poc fiable deixa de ser una molèstia i passa a ser el fre principal. Un agent treballa en bucle (genera, executa la suite, llegeix l’error, corregeix), i cada volta del bucle costa el que costa la suite. Amb proves ràpides el bucle s’aguanta sol; amb proves intermitents el bucle es trenca, perquè l’agent no distingeix un vermell real d’un vermell aleatori i “corregeix” codi que ja era correcte.
La prova és l’especificació que dones a l’agent
A un humà li pots demanar “que gestioni bé els descomptes” i entendrà el context. Un model no en té, de context, més enllà del que li poses al davant, i omplirà els buits amb el que li sembli plausible.
Una prova escrita abans no pateix aquest problema: fixa el comportament esperat en un format que no admet interpretacions i que l’agent pot executar per saber si ha acabat. Això no és exactament TDD, és el mateix mecanisme utilitzat com a contracte de la delegació: tu escrius el criteri, l’agent busca el codi que el compleix.
Tests tautològics
Quan el mateix agent escriu la implementació i les proves, totes dues neixen de la mateixa lectura del problema i comparteixen els mateixos punts cecs. La prova acaba afirmant el que el codi fa, no el que hauria de fer, i passa sempre. Són els tests tautològics, i com evitar-los es tracta a Testing amb IA.
El senyal d’alerta és fàcil de reconèixer: si demanes proves per a codi ja escrit i totes passen a la primera, no has verificat res, has fotografiat el comportament actual.
Què guanya i què perd valor
| Pràctica | Per què canvia |
|---|---|
| Escriure la prova abans | Guanya: és la manera de dir a l’agent què vols, i garanteix que la prova no s’ha copiat del codi |
| Cobertura com a objectiu | Perd: un agent arriba al 90% sense esforç i sense verificar res. Mai no ha estat bona mètrica, però ara és trivial de falsejar |
| Mutacions | Guanya: pregunta si la suite detecta bugs, que és exactament el que la cobertura no respon |
| Proves que travessen fronteres | Guanyen: escrites amb vocabulari de domini, no poden heretar els punts cecs de la implementació |
| Llegir el diff de les proves | Guanya: revisar les proves passa a ser més informatiu que revisar la implementació, perquè hi has de comprovar que afirmen el que toca |
| Suite ràpida i determinista | Guanya: és la condició perquè el bucle de l’agent funcioni |
El que no canvia
Una prova segueix sent codi que algú haurà de mantenir, i que un agent en generi cent en un minut no vol dir que en calguin cent. Una suite inflada de proves redundants és més lenta, més fràgil i amaga les que sí que importen.
La responsabilitat tampoc es delega: el criteri de què s’ha de complir continua sent teu. L’agent proposa el codi, i de vegades la prova, però qui decideix que la prova és la correcta és qui signa el commit.