Commit f0a30195 authored by jg5dev's avatar jg5dev 💬
Browse files

better

parent 7ca2d12f
Loading
Loading
Loading
Loading
+175 −29
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

<!-- toc -->

Tenir codi llest per a producció requereix fer proves. Com que no podem tenir milers de testers, 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](./devops.md#automatització-del-lliurament).
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](./devops.md#automatització-del-lliurament).

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](../java/eines_java.md#junit) per a Java, [Vitest i Testing Library](../m7/uf2/proves.md) per a JavaScript.

@@ -33,7 +33,7 @@ Cada nivell atrapa una classe de fallada diferent, i el criteri per repartir-hi

## 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 unitària pot ser, alhora, una prova de regressió i una prova de fum.
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ó

@@ -65,29 +65,12 @@ La resta de noms que sentiràs es distingeixen per la pregunta que responen, no
| **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](./fronteres_servei.md) |

## 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](./devops.md#git-hooks) | Linters i les proves unitàries ràpides | Menys d'un minut | Tu |
| A cada push, a la [integració contínua](./devops.md#integració-contínua-ci) | 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](./devops.md#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](./devops.md#automatització-del-lliurament).

## 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 nostre codi amb funcions petites ens ajuda a fer proves més granulars.
- 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](./disseny.md#disseny-per-a-la-testabilitat) i a [Llegir els senyals de les proves](./arquitectura.md#llegir-els-senyals-de-les-proves).
@@ -100,7 +83,7 @@ L'estructura recomanada per a qualsevol prova, sigui del nivell que sigui, és u
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](./disseny.md#el-criteri-de-correcció). Els tres blocs es distingeixen visualment amb una línia en blanc:
També es coneix com a **given, when, then**, que és el mateix patró amb el vocabulari del [BDD](#bdd-i-atdd). Els tres blocs es distingeixen visualment amb una línia en blanc:

```java
@Test
@@ -183,34 +166,140 @@ El TDD (desenvolupament guiat per proves) inverteix l'ordre habitual: primer s'e

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](#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:

```java
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:

```java
@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:

```java
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*, prou llegible perquè algú que no programa el pugui validar:
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](#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.

```gherkin
Escenari: retirada per sobre del saldo
# 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
  Llavors l'operació es rebutja
    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     |
```

Aquest text no és documentació: hi ha eines (Cucumber a Java, entre d'altres) que l'executen enllaçant cada línia amb un mètode. Quan l'escenari s'acorda i es fa executable, es parla d'**especificació amb exemples** (specification by example): el mateix document és el requisit, la prova i la documentació, i per tant no es pot desactualitzar sense que la suite es posi vermella.
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`.

El cost és real: mantenir la capa que tradueix el text a codi és feina. Té sentit per a les regles de negoci que el client ha de validar, no per a cada mètode.
#### 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:

```java
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](#com-anomenar-una-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 sense trencar res sense adonar-te'n, i llavors sí, corregir el que estigui malament.
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).
@@ -224,7 +313,9 @@ void elSaldoMaiEsNegatiu(@ForAll @Positive int saldo, @ForAll @Positive int reti
}
```

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. La dificultat de la tècnica no és l'eina, és **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.
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

@@ -299,6 +390,58 @@ Les causes habituals:

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](./devops.md#git-hooks) | Linters i les proves unitàries ràpides | Menys d'un minut | Tu |
| A cada push, a la [integració contínua](./devops.md#integració-contínua-ci) | 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](./devops.md#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](./devops.md#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](#les-proves-dins-del-cicle-de-desenvolupament). 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](#proves-dacceptació) 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](#bdd-i-atdd) 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ó](#proves-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.
@@ -315,7 +458,7 @@ A un humà li pots demanar "que gestioni bé els descomptes" i entendrà el cont

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](#test-driven-development), és el mateix mecanisme utilitzat com a contracte de la delegació: tu escrius el criteri, l'agent busca el codi que el compleix.

### El risc nou: proves que no verifiquen res
### 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](../aitools/07_practiques_i_tancament.md#testing-amb-ia).

@@ -344,5 +487,8 @@ La responsabilitat tampoc es delega: el criteri de què s'ha de complir continua
- [Mocks Aren't Stubs](https://martinfowler.com/articles/mocksArentStubs.html)
- [Test Double (Martin Fowler)](https://martinfowler.com/bliki/TestDouble.html)
- [Eradicating Non-Determinism in Tests](https://martinfowler.com/articles/nonDeterminism.html)
- [Specification by Example](https://martinfowler.com/bliki/SpecificationByExample.html)
- [Gherkin Reference](https://cucumber.io/docs/gherkin/reference)
- [jqwik, property-based testing per a Java](https://jqwik.net/)
- [PIT Mutation Testing](https://pitest.org/)
- [Stryker Mutator](https://stryker-mutator.io/)