Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proves

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:

  1. Proves unitàries: validació dels blocs més atòmics del software, les funcions.
  2. 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.
  3. 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àriesIntegracióEnd-to-end
Objectiu principalFuncions individualsIntegració de funcionsFuncionalitat d’usuari
QuantitatNombrosesUna mica freqüentEscassa
VelocitatMolt altaMitjanaLenta
Freqüència d’execucióAlta, quan es desenvolupa una funcióRegular, quan es desenvolupa una funcionalitatQuan s’acaba una funcionalitat
FeedbackEntrada i sortida per a la funcióComportament problemàticFuncionalitat incorrecta
CostBaix: petita, fàcil d’actualitzar, executar i entendreModeratCostós
Coneixement de l’aplicacióAcoblat al codiCodi, bases de dades, xarxa, arxiusInconscient
BeneficisFeedback ràpid durant el desenvolupament, evitar regressions, documentacióÚs de llibreries de tercers, comprova efectes secundarisFuncionament 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:

TipusQuè 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àrregaAguanta el volum esperat i respon prou ràpid? No verifiquen correcció, sinó límits
Proves de seguretatEs comporta bé davant d’entrades malicioses, no només davant de les previstes?
Proves de contracteLes 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:

  1. Arrange: prepara les entrades i els objectius de la prova.
  2. Act: invoca el comportament, cridant una funció, interactuant amb un API, una pàgina web, etc.
  3. 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àcticaQui escriu la provaNivellQuè fixa
TDDLa persona que programaUnitariEl disseny d’una unitat, pas a pas
BDD i ATDDL’equip amb qui demana la funcionalitatAcceptacióQuè vol dir que la funcionalitat està feta
Proves de caracteritzacióQui ha de tocar codi heretatEl que calguiEl comportament actual, abans de canviar-lo
Proves basades en propietatsLa persona que programaUnitariUna 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:

  1. Afegir un nou test
  2. Executar tots els tests. La nova prova ha de fallar.
  3. Escriure el codi més senzill que permeti passar el test.
  4. Totes les proves han de funcionar novament.
  5. 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 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èticaa + b passa a a - b
Valor de retornreturn x passa a return null o return 0
Eliminació d’una cridas’esborra la invocació d’un mètode void
Incrementi++ 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.

LlenguatgeEina
Java i JVMPIT (pitest)
C#, JavaScript, TypeScript, ScalaStryker
Pythonmutmut, cosmic-ray
PHPInfection
Gogo-mutesting, gremlins
Rustcargo-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.

MomentQuè s’hi executaTemps acceptableQui veu la fallada
Mentre programes, a l’IDELes proves unitàries del que estàs tocantSegonsTu
Abans del commit, amb un git hookLinters i les proves unitàries ràpidesMenys d’un minutTu
A cada push, a la integració contínuaLa suite sencera, incloent-hi integracióMinutsL’equip
Abans de desplegar, a stagingEnd-to-end i acceptacióDesenes de minutsL’equip
Just després de desplegarProves de fum i health checksSegonsQui està de guàrdia
En produccióMonitoratge, desplegaments graduals, feature flagsContinuEls 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:

RolQuè hi aporta
Qui programaLes proves unitàries i d’integració del que escriu, i deixar la branca principal verda
QA o testerDissenyar 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 funcionalitatFixar els criteris d’acceptació i validar que els escenaris diuen el que han de dir
El clientLa prova d’acceptació d’usuari abans de donar per bona una entrega
Qui està de guàrdiaEl 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àcticaPer què canvia
Escriure la prova abansGuanya: és la manera de dir a l’agent què vols, i garanteix que la prova no s’ha copiat del codi
Cobertura com a objectiuPerd: un agent arriba al 90% sense esforç i sense verificar res. Mai no ha estat bona mètrica, però ara és trivial de falsejar
MutacionsGuanya: pregunta si la suite detecta bugs, que és exactament el que la cobertura no respon
Proves que travessen fronteresGuanyen: escrites amb vocabulari de domini, no poden heretar els punts cecs de la implementació
Llegir el diff de les provesGuanya: 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 deterministaGuanya: é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.

Referències

Last change: , commit: f0a3019