Pubblicato il 26 maggio 2026
Best PracticeSoftware DevelopmentTesting

Test-Driven Development (TDD)

Una guida completa al Test-Driven Development (TDD): il ciclo Red-Green-Refactor, i vantaggi del design guidato dai test e come superare le resistenze comuni.

Cos'è il Test-Driven Development (TDD)?

Il Test-Driven Development (TDD), o Sviluppo Guidato dai Test, è una metodologia di ingegneria del software ideata da Kent Beck alla fine degli anni '90.

A differenza del flusso tradizionale in cui si scrive prima il codice di produzione e poi i test per verificarne il funzionamento (spesso come un'attività secondaria o opzionale), il TDD inverte completamente questo paradigma: si scrive il test prima ancora di avere a disposizione il codice che implementa la funzionalità.

Una delle più grandi rivelazioni per chi si approccia al TDD è comprendere che non si tratta principalmente di un'attività di testing, bensì di un'attività di progettazione (design). Scrivere prima il test ti costringe a pensare all'interfaccia pubblica del tuo codice, a come verrà consumato dagli altri moduli e a quali requisiti di business deve rispondere prima ancora di farti distrarre dai dettagli implementativi.


Il Ciclo Red-Green-Refactor

Il TDD si basa su un ciclo iterativo estremamente rapido e ritmato, noto come Red-Green-Refactor (Rosso, Verde, Rifattorizzazione):

graph TD
    A[1. Scrivi un test che fallisce - RED] --> B[2. Scrivi il codice minimo per farlo passare - GREEN]
    B --> C[3. Pulisci e ottimizza il codice - REFACTOR]
    C --> A

1. Fase Rosso (RED)

Scrivi un test unitario focalizzato su un singolo comportamento o requisito. Poiché il codice di produzione per soddisfare tale test non esiste ancora, esegui il test e assicurati che fallisca. Nota: Un test che compila ma fallisce per un'asserzione errata è l'ideale. Se il codice non compila affatto, anche questo conta come "fallimento" (Rosso). Questa fase è cruciale perché dimostra che il test sta effettivamente controllando qualcosa che manca.

2. Fase Verde (GREEN)

Scrivi la quantità minima di codice necessaria per far passare il test. In questa fase non importa se il codice non è elegante, se c'è della duplicazione o se hai utilizzato dei valori scritti a codice (hardcoded). L'unico obiettivo è vedere la barra dei test diventare verde il più rapidamente possibile.

3. Fase Rifattorizzazione (REFACTOR)

Una volta che il test è verde, hai una rete di sicurezza che ti protegge da involontarie regressioni. Ora puoi (e devi) migliorare la qualità del codice. Pulisci le duplicazioni, migliora i nomi delle variabili e delle funzioni, estrai classi se necessario e ottimizza le performance. Esegui continuamente i test per assicurarti che il comportamento esterno rimanga immutato e che la barra rimanga verde.


Un Esempio Pratico in TypeScript

Ipotizziamo di voler creare una classe CalcolatoreSconti che applica uno sconto del 10% se l'importo del carrello supera i 100 euro. Seguiamo il ciclo TDD.

Fase 1: Scriviamo il test che fallisce (RED)

Scriviamo il test assumendo come vorremmo utilizzare la nostra classe:

// calcolatore.test.ts
import { CalcolatoreSconti } from "./calcolatore";

describe("CalcolatoreSconti", () => {
  it("non dovrebbe applicare sconti se l'importo è inferiore o uguale a 100", () => {
    const calcolatore = new CalcolatoreSconti();
    const totale = calcolatore.applicaSconto(80);
    expect(totale).toBe(80);
  });

  it("dovrebbe applicare uno sconto del 10% se l'importo è superiore a 100", () => {
    const calcolatore = new CalcolatoreSconti();
    const totale = calcolatore.applicaSconto(150);
    expect(totale).toBe(135); // 150 - 15 (10%)
  });
});

Se proviamo a lanciare il test, questo fallirà immediatamente perché la classe CalcolatoreSconti non è ancora stata creata. Siamo in fase Rosso.

Fase 2: Scriviamo il codice minimo (GREEN)

Creiamo il file calcolatore.ts e inseriamo il codice più semplice e grezzo possibile per soddisfare i test:

// calcolatore.ts
export class CalcolatoreSconti {
  applicaSconto(importo: number): number {
    if (importo > 100) {
      return importo * 0.9;
    }
    return importo;
  }
}

Eseguiamo nuovamente i test: la barra è Verde! Entrambi i test passano con successo.

Fase 3: Rifattorizziamo il codice (REFACTOR)

Sebbene il codice precedente sia estremamente semplice, ci rendiamo conto che i valori "100" (soglia) e "0.9" (sconto del 10%) sono magic numbers cablati all'interno del metodo. Possiamo migliorare la leggibilità e l'estensibilità estraendoli in costanti o configurandoli nel costruttore:

// calcolatore.ts (Rifattorizzato)
export class CalcolatoreSconti {
  private readonly SOGLIA_SCONTO = 100;
  private readonly PERCENTUALE_SCONTO = 0.10;

  applicaSconto(importo: number): number {
    if (this.haDirittoAlloSconto(importo)) {
      return importo - (importo * this.PERCENTUALE_SCONTO);
    }
    return importo;
  }

  private haDirittoAlloSconto(importo: number): boolean {
    return importo > this.SOGLIA_SCONTO;
  }
}

Lanciando nuovamente la suite di test, verifichiamo che il comportamento non è cambiato e tutto funziona perfettamente. Abbiamo completato il ciclo!


I Vantaggi Principali del TDD

Perché dovresti spendere del tempo a scrivere test prima del codice reale? I vantaggi sul lungo termine sono inestimabili:

  1. Design Modulare e Disaccoppiato: Scrivere prima il test ti impedisce di accoppiare eccessivamente le classi tra loro. Per poter testare un'unità in isolamento, sarai indotto a fare uso di interfacce e iniezione delle dipendenze, migliorando l'architettura complessiva.
  2. Prevenzione dei Bug: Avendo coperto ogni comportamento con un test, ridurrai drasticamente i bug che finiscono in produzione.
  3. Sicurezza nel Refactoring: Questa è forse la sensazione più appagante. Puoi ristrutturare completamente intere aree del codebase sapendo che se commetti un errore, un test te lo segnalerà in pochissimi millisecondi.
  4. Documentazione Vivente: I test descrivono con precisione il comportamento del software in vari scenari. Chiunque entri nel progetto può leggere i test per capire cosa fa il sistema, senza bisogno di documentazione testuale obsoleta.

Superare le Resistenze Comuni

Molti sviluppatori o manager di progetto mostrano scetticismo nei confronti del TDD. Ecco come rispondere alle obiezioni più diffuse:

  • "Scrivere test raddoppia il tempo di sviluppo!" Inizialmente può sembrare così a causa della curva di apprendimento. Tuttavia, il TDD fa risparmiare un'enorme quantità di tempo che altrimenti verrebbe spesa in sessioni estenuanti di debugging manuale, fix di bug in produzione e regressioni inattese.
  • "I nostri requisiti cambiano troppo spesso, dovremmo riscrivere sempre i test." Se i requisiti cambiano, il codice deve cambiare di conseguenza. Cambiare prima il test ti costringe a chiarire come il nuovo requisito influirà sul resto dell'applicazione, fungendo da guida sicura per le modifiche al codice di produzione.

Conclusione

Il Test-Driven Development non è solo una tecnica, è una disciplina e un modo di pensare. Richiede pazienza, pratica e la volontà di rallentare temporaneamente per poter accelerare esponenzialmente in futuro.

Integrando il TDD con concetti come la Clean Architecture e i principi SOLID, sarai in grado di produrre codice robusto, manutenibile e flessibile, capace di adattarsi con estrema agilità a qualsiasi esigenza di business.

Condividi questo articolo