modulo 13 di 24 / Ore 21-40

Moduli 9-16 / Checkpoint 40 ore

Perché testare prima del refactoring

Capire cosa succede quando si fa refactoring senza test: come si introduce una regressione silenziosa, e perché un test scritto prima è la rete di sicurezza del refactoring.

25 min Architettura del software

Perché testare prima del refactoring

Questa lezione appartiene al Modulo 13 — Testare l’architettura.

Il rischio concreto

Il refactoring cambia la struttura del codice senza cambiare il comportamento. Ma come si sa che il comportamento non è cambiato?

Senza test, si sa solo guardando la UI manualmente. Il problema: la UI non esercita tutti i casi. È facile dimenticare uno scenario raro, e quel scenario rompe in produzione.

Ecco cosa succede senza test, passo per passo:

Prima del refactoring:
  - registraPrestito funziona
  - verifica che strumento.stato === "disponibile"
  - salva prestito e aggiorna strumento

Durante il refactoring (estrai puoEsserePrestato in funzione separata):
  - sposti il controllo dello stato in puoEsserePrestato()
  - per sbaglio scrivi strumento.stato === "Disponibile" (D maiuscola)
  - la UI mostra "Prestito registrato" anche per strumenti guasti

Dopo il refactoring:
  - la UI sembra funzionare (il caso felice è ok)
  - il bug è silenzioso — emerge solo con uno strumento guasto

Un test scritto prima avrebbe rilevato il bug immediatamente.

Cosa succede con il test

// Test scritto PRIMA del refactoring
test("uno strumento guasto non può essere prestato", () => {
  const archivioStrumenti = creaArchivioStrumentiInMemoria([
    { id: "s1", nome: "Trapano", categoria: "Elettrico", stato: "guasto" }
  ]);
  const archivioPrestiti = creaArchivioPrestitiInMemoria();
  const registraPrestito = creaRegistraPrestito({ archivioStrumenti, archivioPrestiti });

  expect(() => registraPrestito({ strumentoId: "s1", assegnatario: "Luca 3A" }))
    .toThrow("STRUMENTO_NON_DISPONIBILE");
});

// Ora esegui il refactoring.
// Dopo il refactoring, riesegui il test.
// Se fallisce → hai introdotto una regressione.
// Se passa → il comportamento è invariato.

Il test è la rete di sicurezza. Non elimina il refactoring sbagliato — lo rileva subito.

Tre scenari da testare PRIMA di refactorare

Quando si fa refactoring su una funzione del Registro, questi tre scenari devono essere coperti da test prima di iniziare:

Scenario 1: il caso felice

test("registra un prestito per uno strumento disponibile", async () => {
  const archivioStrumenti = creaArchivioStrumentiInMemoria([
    { id: "s1", nome: "Trapano", categoria: "Elettrico", stato: "disponibile" }
  ]);
  const archivioPrestiti = creaArchivioPrestitiInMemoria();
  const registraPrestito = creaRegistraPrestito({ archivioStrumenti, archivioPrestiti });

  await registraPrestito({ strumentoId: "s1", assegnatario: "Luca 3A" });

  const strumentoAggiornato = archivioStrumenti.trovaPerId("s1");
  expect(strumentoAggiornato.stato).toBe("in_prestito");
});

Scenario 2: la regola violata

test("non registra un prestito per uno strumento guasto", async () => {
  const archivioStrumenti = creaArchivioStrumentiInMemoria([
    { id: "s1", nome: "Trapano", categoria: "Elettrico", stato: "guasto" }
  ]);
  const registraPrestito = creaRegistraPrestito({
    archivioStrumenti,
    archivioPrestiti: creaArchivioPrestitiInMemoria()
  });

  await expect(registraPrestito({ strumentoId: "s1", assegnatario: "Luca 3A" }))
    .rejects.toThrow("STRUMENTO_NON_DISPONIBILE");
});

Scenario 3: l’oggetto non trovato

test("non registra un prestito per uno strumento inesistente", async () => {
  const archivioStrumenti = creaArchivioStrumentiInMemoria([]); // archivio vuoto
  const registraPrestito = creaRegistraPrestito({
    archivioStrumenti,
    archivioPrestiti: creaArchivioPrestitiInMemoria()
  });

  await expect(registraPrestito({ strumentoId: "str-999", assegnatario: "Luca 3A" }))
    .rejects.toThrow("STRUMENTO_NON_TROVATO");
});

Con questi tre test scritti prima, il refactoring è protetto. Se dopo il refactoring tutti e tre passano, il comportamento è invariato.

Quando NON serve scrivere test prima

Non ogni riga di codice ha bisogno di un test prima di cambiare:

  • rinomino di variabile o funzione → il comportamento non cambia, basta verificare che compili
  • cambiamento di commento o documentazione → nessun test necessario
  • aggiunta di console.log temporaneo → nessun test necessario

I test prima del refactoring servono quando si cambia la struttura di una funzione che contiene logica di dominio o di coordinamento (caso d’uso).

Il workflow corretto

1. Identifica la funzione da refactorare
2. Scrivi (o verifica che esistano) test per: caso felice, regola violata, oggetto non trovato
3. Esegui i test → devono passare tutti (se non passano, il codice era già rotto)
4. Esegui il refactoring
5. Riesegui i test → devono ancora passare tutti
6. Se un test fallisce → il refactoring ha introdotto una regressione → risolvi

Procedura guidata

  1. Scegli la funzione che vuoi refactorare nel Registro.
  2. Scrivi i tre test (caso felice, regola violata, oggetto non trovato) prima di toccare il codice.
  3. Esegui i test — devono passare.
  4. Esegui il refactoring pianificato.
  5. Riesegui i test — devono ancora passare.

Attività in classe

## Test scritti prima del refactoring

Funzione da refactorare:

Test 1 — caso felice:
  - Scenario: 
  - Comportamento atteso: 
  - Passa prima del refactoring (sì/no): 

Test 2 — regola violata:
  - Scenario: 
  - Comportamento atteso: 
  - Passa prima del refactoring (sì/no): 

Test 3 — oggetto non trovato:
  - Scenario: 
  - Comportamento atteso: 
  - Passa prima del refactoring (sì/no): 

Refactoring eseguito:

Test dopo il refactoring (tutti passano? sì/no):

Spiegazione guidata per studiare

Il refactoring senza test è come spostare i mobili al buio: probabilmente va bene, ma non lo sai finché non accendi la luce. Il test è la luce.

Il punto centrale è: il test scritto prima non è documentazione — è una rete di sicurezza. Se il test passa prima e dopo il refactoring, hai la prova che il comportamento non è cambiato. Se fallisce dopo, hai trovato esattamente dov’è il problema.

Il rischio tipico è pensare “so cosa fa questo codice, non ho bisogno di test”. Questa confidenza è tradita dal bug più sottile: una maiuscola al posto di una minuscola, un operatore === invece di ==, un return nel posto sbagliato. Il test trova questi bug in secondi. La UI manuale li trova in ore — o non li trova affatto.

Passo per passo nel progetto

  1. Scegli una funzione del dominio o un caso d’uso che vuoi refactorare.
  2. Prima di toccare il codice: esistono test per questa funzione? Eseguili.
  3. Se non esistono: scrivi i tre scenari minimi (felice, regola violata, non trovato).
  4. Verifica che i test passino — se non passano, il codice è già rotto.
  5. Esegui il refactoring, un passo alla volta.
  6. Dopo ogni passo: riesegui i test.

Esempio da leggere lentamente

// Situazione: voglio estrarre puoEsserePrestato in una funzione separata.
// Scrivo il test PRIMA di estrarre.

test("puoEsserePrestato lancia per strumento non disponibile", () => {
  const guasto = { id: "s1", stato: "guasto" };
  const inPrestito = { id: "s2", stato: "in_prestito" };
  const disponibile = { id: "s3", stato: "disponibile" };

  expect(() => puoEsserePrestato(guasto)).toThrow();
  expect(() => puoEsserePrestato(inPrestito)).toThrow();
  expect(() => puoEsserePrestato(disponibile)).not.toThrow();
});

// Ora estraggo puoEsserePrestato.
// Rieseguo il test.
// Se passa → il refactoring è corretto.

Prova di comprensione

  • Senza test, come puoi sapere che il refactoring non ha rotto nulla?
  • Quali tre scenari devono essere coperti prima di refactorare registraPrestito?
  • Perché un test che passa prima del refactoring e fallisce dopo è una buona notizia?

Errori da evitare

  • scrivere i test dopo il refactoring — non protegge, documenta solo a posteriori;
  • testare solo il caso felice — le regressioni si nascondono nei casi limite;
  • non eseguire i test prima di iniziare — se il codice era già rotto, il refactoring non è il problema;
  • fare refactoring e aggiungere funzionalità nello stesso passo — separa le due operazioni.

Prodotto da consegnare

Può essere:

  • tre test scritti per la funzione da refactorare, con verifica che passino prima del refactoring;
  • il log di un’esecuzione dei test prima e dopo il refactoring;
  • la scheda compilata con i risultati.

Checklist di chiusura

  • Ho identificato la funzione da refactorare
  • Ho scritto il test del caso felice prima di toccare il codice
  • Ho scritto il test della regola violata
  • Ho scritto il test dell’oggetto non trovato
  • Tutti i test passano prima del refactoring
  • Tutti i test passano dopo il refactoring

Domande per la revisione

  • I test che hai scritto erano sufficienti per rilevare le regressioni introdotte?
  • Hai trovato un bug nel codice esistente mentre scrivevi i test? (Succede spesso.)
  • Quale dei tre scenari era più difficile da testare nel tuo progetto?

Risultato atteso

Alla fine lo studente deve saper dire:

Ho scritto questi tre test per [funzione] prima del refactoring:
1. Caso felice: [scenario] → il test passa.
2. Regola violata: [scenario] → il test passa.
3. Oggetto non trovato: [scenario] → il test passa.
Dopo il refactoring, tutti e tre i test passano ancora.

Se questa spiegazione non è possibile, la lezione non è ancora davvero conclusa.