Sviluppatore software con elmetto che lavora su codice legacy

Abbiamo già parlato di come affrontare il codice legacy in un precedente articolo, spiegando come piccole azioni di refactoring possano aiutarci nel comprendere e successivamente modificare parti del sistema poco conosciute e ad elevata complessità cognitiva.

Tutto ciò di cui abbiamo già parlato però, partiva da un presupposto ben preciso: Il sistema doveva essere coperto da una buona base di test automatici che ci garantissero che il comportamento atteso rimanesse inalterato.

Quando il codice legacy non è testato

Purtroppo, sappiamo bene che spesso non è questo il caso, e quindi ci ritroviamo a dover capire e modificare una codebase senza uno straccio di test a corredo.
Avventurarci in azioni di refactoring in questa situazione diventerebbe un rischio considerevole perché non avremmo nulla che ci garantisca che il nostro intervento non abbia rotto in qualche modo le funzionalità già presenti, e magari in produzione da diverso tempo.

Come possiamo agire in un contesto simile?

La realtà è che non abbiamo una soluzione semplice e la strada è tanto scontata quanto impegnativa da percorrere: dobbiamo aggiungere dei test che fungano da rete di protezione per i cambiamenti che andremo ad implementare.
Il problema in questo caso, è che se non siamo a conoscenza dei comportamenti attuali, siamo in difficoltà anche nel pensare a quali test si possono scrivere, prima ancora di iniziare a lavorare sulla codebase.
Il primo passo sarà quindi mettere in esercizio il sistema e capirne il funzionamento, vediamo insieme quali tecniche possono venirci in soccorso.

I test di caratterizzazione

I test di caratterizzazione (conosciuti anche come test Golden Master) sono atti ad esplorare il comportamento del sistema senza che lo si conosca a priori.
Se negli unit test tipicamente mettiamo in esercizio un’unità confrontando il risultato ottenuto con uno atteso, nel caso dei test di caratterizzazione non abbiamo un vero risultato atteso, ma dobbiamo assumere che, qualunque sia il risultato ottenuto dal software legacy, sia quello corretto.
È importante sottolineare che per aumentare il livello di comprensione del sistema, occorre creare tanti test di caratterizzazione, con diversi input e provando a pensare a quali casi particolari potrebbero portare a risultati diversi, come se avessimo fra le mani un gioco rompicapo da risolvere e proviamo tutti i modi che ci vengono in mente per metterlo sotto stress, capirlo fino in fondo e infine trovare la soluzione.

Non è scontato avere la sensibilità del livello di comprensione raggiunto dai nostri test di caratterizzazione, d’altronde non sapendo a prescindere il funzionamento, è anche difficile sapere quali casi particolari potrebbero metterlo in crisi o restituire risultati di natura diversa.
Pur non essendoci una soluzione “scientifica” a questo problema, possiamo avere una misura di completezza grazie alla code coverage raggiunta dai nostri test di caratterizzazione. Più alta sarà la code coverage, maggiori saranno le probabilità di essere entrati in tutti i “rami” di logica esistenti e aver coperto tutti i casi particolari.

Gli approval test

Finora abbiamo parlato di test che mettono in esercizio il sistema e ci aiutano a comprenderlo, ma ancora non abbiamo visto come poter creare una rete di protezione per i successivi interventi di refactoring che andremo a svolgere.
In realtà, se ci pensate, abbiamo detto che gli output ottenuti nei test di caratterizzazione dobbiamo assumere che siano corretti e che rispecchino il comportamento atteso, sarebbe quindi sufficiente salvarci il risultato ottenuto e confrontarlo con quello che viene restituito successivamente alle nostre modifiche. In questo modo, ad ogni run dei test, ci assicuriamo che i risultati ottenuti dalla situazione attuale siano ancora coerenti con quelli del sistema originale.

Le librerie di approval test vanno in questa direzione.
A differenza delle normali “Assert” che si usano negli unit test, l’approval test prende l’intero risultato del sistema in esercizio, che sia un singolo numero o un oggetto complesso, e lo serializza in un file di approvazione. Alla successiva esecuzione del test, l’approval test andrà a confrontare il nuovo risultato ottenuto con quello salvato in precedenza e, se i due differiscono in qualche modo, il test fallisce.

Grazie a questo approccio, lo sviluppatore non ha l’onere di pensare alle asserzioni da implementare ed è sicuro che il sistema continui a funzionare come in precedenza. Inoltre, grazie ad alcune configurazioni delle librerie di approval test, è possibile avere un risultato visuale tramite software di comparazione, per avere subito in evidenza i punti in cui gli output divergono.
Naturalmente, si può anche rendere il nuovo risultato quello approvato, invalidando il precedente, e le nuove esecuzioni dei test verranno confrontate con quest’ultimo.

Per chi mastica lo sviluppo web frontend, il concetto degli approval test è avvicinabile a quello degli snapshot test, dove viene confrontato l’HTML generato dalla pagina web rispetto ad uno snapshot generato in precedenza. Nel caso degli approval test, lo snapshot è la serializzazione dell’output ottenuto dal sistema messo in esercizio.

Se siete curiosi di approfondire gli approval test e verificare quali linguaggi sono supportati, vi lasciamo un link di riferimento https://approvaltests.com/

Costruisci il tuo percorso verso l’eccellenza tecnica