modulo 05 / lezione 26

Correggere senza regressioni

Test di regressione che fallisce prima del fix

Trasformare il bug trovato in una verifica automatica che fallisce prima del fix e passa dopo. Test-first bug fix.

65 min Debugging

In parole semplici

Un fix senza test è una scommessa. Funziona oggi, ma niente impedisce al bug di tornare tra sei mesi quando qualcuno modificherà quella parte. La pratica test-first bug fix chiude il cerchio:

  1. Scrivi il test che fallisce a causa del bug.
  2. Verifichi che fallisca.
  3. Applichi il fix.
  4. Verifichi che ora passi.

Il test diventa la guardia perenne contro la regressione.

Parole nuove

  • test di regressione: test scritto per impedire al bug di tornare.
  • test unitario: test che verifica una singola funzione/modulo.
  • test E2E (end-to-end): test che simula l’utente completo.
  • snapshot test: test che confronta l’output con una versione salvata.
  • TDD bug fix: variante del Test Driven Development applicata al fix.

Cosa impari

  • Scrivere un test che cattura un bug specifico.
  • Scegliere il livello giusto: unitario, integrazione, E2E.
  • Conoscere strumenti pratici per JS/web (Vitest, Jest, Playwright, Cypress).

Il ciclo “test-first bug fix”

1. Riproduco il bug a mano   →  capisco i passi
2. Scrivo un test            →  formalizzo i passi
3. Eseguo: deve fallire      →  test "rosso"
4. Applico il fix
5. Eseguo: deve passare      →  test "verde"
6. Commit con fix + test

Il passaggio 3 è fondamentale: se il test passa prima del fix, non sta testando il bug. Stai testando qualcos’altro.

Esempio: unitario

Bug: calcolaSconto(prezzo, null) ritorna NaN.

Test (con Vitest)

import { describe, it, expect } from "vitest";
import { calcolaSconto } from "./sconto.js";

describe("calcolaSconto", () => {
  it("ritorna il prezzo invariato quando la percentuale è null", () => {
    expect(calcolaSconto(100, null)).toBe(100);
  });

  it("ritorna il prezzo invariato quando la percentuale è undefined", () => {
    expect(calcolaSconto(100, undefined)).toBe(100);
  });
});

Esegui:

npx vitest run sconto.test.js

Output:

FAIL  sconto.test.js
  ✗ ritorna il prezzo invariato quando la percentuale è null
    Expected: 100
    Received: NaN

Test rosso. Applico il fix. Rieseguo:

PASS  sconto.test.js
  ✓ ritorna il prezzo invariato quando la percentuale è null
  ✓ ritorna il prezzo invariato quando la percentuale è undefined

Esempio: E2E con Playwright

Bug: dopo il login, il pulsante “Profilo” non appare nella navbar.

import { test, expect } from "@playwright/test";

test("dopo il login appare il pulsante Profilo", async ({ page }) => {
  await page.goto("/login");
  await page.fill('input[name="email"]', "test@example.com");
  await page.fill('input[name="password"]', "password");
  await page.click('button[type="submit"]');

  await expect(page.locator('text=Profilo')).toBeVisible();
});

Test E2E: lento ma realistico. Cattura bug che attraversano client + server + UI.

Scegliere il livello

LivelloVelocitàCoverageQuando usarlo
unitariomolto veloce (ms)bassabug in una funzione isolata
integrazionemedio (secondi)mediabug tra moduli (store + componente)
E2Elento (decine di secondi)altabug che attraversa l’intero stack

Regola: scegli il livello più basso che cattura il bug. Un test unitario ben fatto è meglio di un E2E lento.

Snapshot test

Per UI: salvi l’output di un componente e verifichi che non cambi.

expect(rendiCard(prodotto)).toMatchSnapshot();

Utile per regressioni visuali, ma con cautela: ogni cambio richiede di “aggiornare lo snapshot”, e gli aggiornamenti distratti nascondono regressioni vere.

Cosa fare se il test è difficile da scrivere

Spesso il fatto stesso che un test sia complicato è un segnale: il codice è troppo accoppiato. Indagine:

  • Posso isolare la funzione che ha il bug?
  • Posso mockare le dipendenze esterne?
  • Posso pubblicare l’API per renderla testabile?

A volte la prima cosa da fare è una piccola refattorizzazione preparatoria, in un commit separato (modulo 5.1).

Test mantenibili

Buon test:

  • Nome descrittivo: spiega il caso, non l’implementazione.
  • Una asserzione principale: leggibile a colpo d’occhio.
  • Fixture esplicite: usa dati di test minimi e nominati.
  • Indipendente da altri test: non dipende dall’ordine.

Cattivo test:

  • “test1”, “test_function”.
  • 50 asserzioni in cascata.
  • Dipende da un test precedente che ha lasciato dato in DB.
  • Si rompe per refactor che non cambiano comportamento.

Esercizio guidato

Prendi un bug che hai risolto a mano in passato.

  1. Scrivi il test che lo avrebbe colto.
  2. Reverti il fix (git revert o “rompi” di nuovo).
  3. Esegui il test: rosso?
  4. Riapplica il fix.
  5. Esegui il test: verde?
  6. Commit con fix + test insieme.

Se il test era verde anche prima di reapplicare il fix, non stai testando il bug. Rifallo.

Quando il test è impossibile

Alcuni bug sono difficilissimi da testare automaticamente (effetti visivi sottili, performance percepita). In quei casi:

  • Scrivi una checklist manuale (modulo 5.3) come fallback.
  • Aggiungi metriche/log (modulo 9) che permettano di rilevare la regressione anche senza test.

Non rinunciare alla guardia, scegli un’altra guardia.

Errori frequenti del principiante

  • Saltare il “deve fallire prima”. Senza, non sai se il test serve.
  • Test che testa l’implementazione, non il comportamento. Si rompe con refactor innocui.
  • Una asserzione per cento bug. Se cambia uno, fallisce tutto e non capisci quale.
  • Test E2E per ogni bug. Lenti. Spesso bastano test unitari.