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.
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:
- Scrivi il test che fallisce a causa del bug.
- Verifichi che fallisca.
- Applichi il fix.
- 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
| Livello | Velocità | Coverage | Quando usarlo |
|---|---|---|---|
| unitario | molto veloce (ms) | bassa | bug in una funzione isolata |
| integrazione | medio (secondi) | media | bug tra moduli (store + componente) |
| E2E | lento (decine di secondi) | alta | bug 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.
- Scrivi il test che lo avrebbe colto.
- Reverti il fix (
git reverto “rompi” di nuovo). - Esegui il test: rosso?
- Riapplica il fix.
- Esegui il test: verde?
- 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.