Se il codice sorgente di un’applicazione ne rappresenta la codifica del funzionamento, i test rappresentano la codifica delle aspettative sul funzionamento. È facile scrivere codice che funziona ma è molto più difficile scriverlo in modo che faccia esattamente cosa ci aspettiamo. Quindi, se è vero che i test verificano il codice, è ancora più vero che rappresentano la memoria di quello che prevediamo che accada durante l’esecuzione. Non c’è da stupirsi quindi se i test sono una delle parti più importanti all’interno della codebase delle nostre applicazioni e che dovremmo proteggerli.
Il ciclo di vita dei test
Scriviamo un test quando vogliamo assicurarci che una nuova parte dell’applicazione (una funzione, una classe, un’API) faccia quello che deve. Una volta che il test è stato scritto il suo codice viene eseguito ogni volta che modifichiamo l’applicazione per verificare che il comportamento previsto sia rimasto invariato (es. continuous integration). Quando riscriviamo una parte dell’applicazione i test sono lo strumento che ci permette di farlo senza pensieri (a patto che coprano tutte le parti che vogliamo cambiare). Un test viene modificato quando troviamo un modo più semplice di implementarlo, quando il metodo o la classe testata cambiano, o quando è eliminato perché non serve più.
Obiettivi sbagliati
Per un programmatore la pigrizia è una delle tre virtù essenziali; questa è anche quella che, di solito, permette ai test di essere (ben) scritti e poi dimenticati (almeno fino a una delle eventualità citate nelle righe precedenti). Negli ultimi anni però si è diffuso l’utilizzo di generatori di codice (aka LLM) e molti sviluppatori hanno iniziato a usarli anche per la scrittura dei test. In alcune aziende ci sono addirittura dei diktat che impongono l’utilizzo di questi strumenti per la scrittura dei test, così da raggiungere il sacro Graal dell’elevata percentuale di code coverage. Questa metrica è però quantitativa e non qualitativa, ci informa soltanto se l’esecuzione dei test riesce a “toccare” ogni linea di codice dell’applicazione. Ma non è questo (ancora) il problema.
Strumenti sbagliati
Il vero problema dell’utilizzo degli LLM per la generazione automatica del codice è che, appunto, il loro instancabile ciclo di scrittura e riscrittura non risparmia nessun tipo di file, nemmeno i test. E proprio perché questa operazione non richiede (apparente) sforzo umano, gli strumenti automatici vengono lasciati liberi di fare tutto quello che ritengono opportuno. Tanto c’è la code coverage a proteggerci, no?
Presupposti sbagliati
È a questo punto che si verifica il cortocircuito e che crolla il castello di presupposti su cui basiamo la validazione del software. Quando viene generato un test come attività collaterale alla creazione di una nuova funzionalità, o quando viene fatto in seguito a un refactor automatico, la sicurezza offerta dai test viene meno. Nonostante la code coverage rimanga elevata o aumenti, la riscrittura del test facilita la perdita del senso originale per cui era stato scritto. Alcuni test sono ovvi, altri meno e altri ancora servono a simulare quella circostanza molto particolare che aveva causato un problema in produzione.
Misurare per prevenire
Per iniziare a intercettare in modo automatico queste situazioni, causate da esseri umani e non, è possibile tenere traccia del numero di commit che riguardano la parte applicativa e la parte dei test del progetto. In una situazione “normale”, il numero di revisioni al codice applicativo sarà sempre superiore rispetto a quello dei test. Se invece i due numeri viaggiano sulla stessa linea o, peggio, se il numero di commit sui test supera quelli per l’applicazione, c’è un problema. Mettondolo in modo un po’ più formale, potremmo dire che il rapporto tra le revisioni applicative e quelle sui test deve essere maggiore di 1.
(numero di revisioni applicative)/(numero di revisioni sui test) > 1
Una misura più precisa
Se con l’analisi temporale del numero di revisioni dei test abbiamo un indicatore generale su tutto il progetto, analizzando il code churn possiamo trovare quali test sono esposti ai rischi di cui abbiamo discusso sopra.
Il concetto di code churn su un file ci indica quanto un il suo codice sia stato modificato nel tempo con aggiunte, sostituzioni o rimozioni.
Git ci fornisce questa informazione con il comando log e l’opzione --stat, mostrandoci i + per le aggiunte e i - per le rimozioni.
L’informazione viene mostrata per ogni file e per ogni commit.
git log --pretty=format:'[%h] %aN %ad %s' --date=short --stat
...
app.js | 16 +++++++-----
package.json | 12 ++++-----
lib/i18n.js | 4 ++-
test/core.js | 20 ++++++++++----------
Con l’utilizzo di strumenti automatici (facilitati dall’output machine readable dell’opzione --numstat) è possibile catturare queste informazioni, metterle insieme, e ottenere così una vista temporale sull’andamento del code churn.
Quando il numero di aggiunte e rimozioni dai file di test è alto e frequente, vuol dire che questi sono a rischio e lo è anche la nostra applicazione.
Conclusioni
Non è detto che il problema del code churn sui test sia per forza da attribuire agli LLM (anche se oggi ne rappresentano la causa più probabile), magari sono stati progettati male o magari chi ha scritto quel codice non aveva le idee molto chiare… Qualunque sia la causa, i test devono essere protetti da modifiche non necessarie al fine di mantenere il loro valore e la loro utilità. L’analisi del code churn può essere usata anche per identificare altri tipi di problemi nel codice applicativo, non solo nei test, ma questa è un’altra storia.