Test di Caratterizzazione: Come Aggiungere Copertura ai Sistemi Legacy
Come scrivere test di caratterizzazione per aggiungere copertura in sicurezza al codice legacy prima del refactoring, usando golden master e approval testing.
In questo articolo:
- Il problema del testing sul codice legacy
- Cosa sono i test di caratterizzazione e come funzionano
- Passo per passo: scrivere il primo test di caratterizzazione
- Golden master testing e approval test
- Conclusione
I test di caratterizzazione risolvono un problema specifico: come fare refactoring in sicurezza del codice legacy che non ha test? La risposta non è “scrivi test che verificano cosa dovrebbe fare il codice.” Nei sistemi legacy, spesso non sai cosa dovrebbe fare il codice. Sai solo cosa fa attualmente. I test di caratterizzazione catturano quel comportamento attuale, creando una rete di sicurezza che ti dice se il tuo refactoring ha cambiato qualcosa.
Questa tecnica è il punto di partenza standard per qualsiasi serio sforzo di modernizzazione del codice legacy. Se il tuo team lavora su sistemi privi di copertura e ha bisogno di migliorarli senza rompere la produzione, i test di caratterizzazione sono il prerequisito.
Il problema del testing sul codice legacy
Il codice legacy spesso ha alta complessità ciclomatica, nessuna dependency injection, stato globale e chiamate al database incorporate nella logica di business. Scrivere test unitari per tale codice nel senso tradizionale richiede un database funzionante, una complessa configurazione di mock, o un ambiente di integrazione completo. Nessuno di questi è veloce da configurare, e tutti richiedono di capire il codice prima di averlo testato.
La conseguenza abituale è che i team saltano completamente i test (“li aggiungiamo dopo il refactoring”) o trascorrono settimane cercando di costruire un’infrastruttura di test prima di poter apportare miglioramenti. Entrambi i percorsi portano allo stesso posto: codice che non viene migliorato, o codice che viene “migliorato” e poi rompe la produzione.
I test di caratterizzazione tagliano questo nodo invertendo la domanda abituale. Invece di chiedere “cosa dovrebbe fare questo codice?” si chiede “cosa fa attualmente questo codice?” La risposta diventa il test.
Cosa sono i test di caratterizzazione e come funzionano
Un test di caratterizzazione, come descritto da Michael Feathers in “Working Effectively with Legacy Code,” è un test che cattura il comportamento attuale di un pezzo di codice senza giudicare se quel comportamento sia corretto.
Il processo è:
- Chiama il codice sotto test con input specifici.
- Osserva gli output (valori di ritorno, effetti collaterali, scritture sul database, output dei log).
- Scrivi asserzioni su quegli output osservati.
- Il test passa quando il codice si comporta esattamente come si comportava quando hai scritto il test.
Se il codice ha un bug, il test di caratterizzazione catturerà il comportamento con il bug. Questo è intenzionale. L’obiettivo non è ancora correggere i bug; l’obiettivo è creare un rilevatore di regressioni in modo da poter fare refactoring senza introdurre nuovi problemi.
Ad esempio, se una funzione restituisce None quando viene passata una stringa vuota, anche se sembra sbagliato, il tuo test di caratterizzazione asserta che function("") == None. Quando in seguito fai refactoring della funzione e inizia a restituire una lista vuota, il test fallisce. Ora sai che il tuo refactoring ha cambiato comportamento, il che ti dà una scelta: quel cambiamento era intenzionale o accidentale?
Passo per passo: scrivere il primo test di caratterizzazione
Passo 1: Identifica il confine. Scegli un metodo, una funzione o una classe da caratterizzare. Inizia con la più piccola unità che puoi isolare: un singolo metodo piuttosto che un’intera classe, una singola classe piuttosto che un intero modulo.
Passo 2: Esegui il codice e osserva. Chiama la funzione con input rappresentativi. Se la funzione richiede accesso al database o servizi esterni, eseguila contro un database di test o usa uno strumento di record-and-replay per catturare le risposte.
Passo 3: Scrivi asserzioni sugli output osservati. Scrivi un test che chiama la funzione con gli stessi input e asserta che gli output corrispondano a quelli osservati. Se la funzione restituisce un oggetto complesso, serializzalo in JSON o in una rappresentazione stringa e asserta su quella.
Passo 4: Esegui il test finché non passa in modo consistente. Se la funzione ha comportamento non deterministico (timestamp, numeri casuali, chiamate esterne), devi controllare quelli prima che il test possa essere affidabile.
Passo 5: Espandi la copertura ai casi limite. Una volta coperto il percorso principale, aggiungi test per i casi limite che riesci a identificare: input vuoti, valori null, condizioni limite. Ogni caso aggiuntivo rende la tua rete di sicurezza più robusta.
Dopo questo processo, hai un insieme di test che rileveranno qualsiasi cambiamento comportamentale nel codice. Ora puoi fare refactoring. Consulta la guida alla legacy modernization per come sequenziare il refactoring dopo la caratterizzazione.
Golden master testing e approval test
Il golden master testing è una variante dei test di caratterizzazione che cattura l’intero output di un sistema piuttosto che asserzioni specifiche. Esegui il sistema con un insieme di input, salvi l’output completo in un file (il “golden master”), e le esecuzioni future dei test confrontano l’output corrente con il file salvato.
Questo è particolarmente utile per funzioni che producono output complessi: report, HTML, XML, strutture JSON con molti campi. Invece di scrivere decine di asserzioni individuali, catturi l’intero output una volta e asserti che non sia cambiato niente.
Il limite del golden master testing è che il golden master può diventare obsoleto quando il comportamento cambia legittimamente. Strumenti come ApprovalTests (disponibili per Java, C#, Python e altri linguaggi) automatizzano questo workflow: quando un test fallisce perché l’output è cambiato, puoi rivedere la diff e “approvare” il nuovo output, aggiornando il golden master.
Il workflow è: esegui il test, guarda la diff, decidi se il cambiamento era intenzionale, approva se sì, correggi il codice se no. Questo rende il golden master testing particolarmente utile per le sessioni di refactoring dove l’obiettivo è esplicitamente preservare il comportamento.
Conclusione
I test di caratterizzazione sono la risposta pratica alla domanda su come fare refactoring in sicurezza del codice legacy. Non richiedono di capire il codice; richiedono di eseguirlo e osservarlo. Non richiedono un’architettura pulita; richiedono un modo per chiamare il codice sotto test e catturare il suo output.
Applicati in modo consistente, i test di caratterizzazione trasformano un codebase con zero copertura in uno con abbastanza copertura per fare refactoring in sicurezza. I team che usano questa tecnica raggiungono tipicamente una copertura funzionale del 60-80% di un modulo legacy entro uno o due giorni, abilitando un refactoring fiducioso che prima era impossibile.
Hai un codebase con questi problemi? Parliamo del tuo sistema