🕐 Tempo di lettura: 6 minuti

«Ma mi servono davvero dieci sviluppatori?»

Chi paga il conto di un progetto, o ne coordina le risorse, questa domanda se l'è fatta almeno una volta. E la risposta è: forse no. Spesso però quei dieci non stanno costruendo nulla di nuovo – stanno tenendo in piedi un carrozzone legacy che nessuno osa toccare, perché nessuno sa cosa si rompe quando lo tocchi. Il modo per averne bisogno di meno esiste, ed è avere codice di qualità. E il codice di qualità, prima di tutto, è codice testato.

Con la pillola #27 ho salutato DetectION, e mi sembrava un buon finale di stagione. Siamo a ottobre, quindi si riparte: la terza stagione si apre con una sottorubrica, Dizionario comune.

L'idea nasce da una conferenza interna di ION di un paio di giorni fa, dove ero tra gli speaker di un panel davanti a una platea di nuovi colleghi. Tra i temi della giornata c'erano i profili T-shaped: la barra verticale della T è la competenza in cui sei specializzato – nel mio caso il backend – quella orizzontale è la capacità di muoverti tra le discipline vicine e di capirti con chi le pratica. Era vero già prima, ma con l'avvento dell'AI lo è più che mai: verticalizzarsi non basta, bisogna anche "contaminarsi" in orizzontale. E il primo passo è imparare un dizionario comune.

Pillole sempre tecniche, quindi, ma pensate anche per chi il codice non lo tocca e con gli sviluppatori ci lavora ogni giorno – analisti funzionali o di business, PO, PM ecc. Quando le parole significano la stessa cosa per tutti, si collabora meglio. E si discute meno sulle stime.

Le due espressioni di oggi sono test di unità e test di integrazione. Con una premessa per i colleghi sviluppatori: la pillola è anche per noi, perché di test parliamo tutti molto volentieri – a scriverli siamo un po' meno.

📌 Codice che controlla altro codice

Un test è semplicemente codice che verifica altro codice. Chiama una funzione, le passa un input e controlla che l'output sia quello atteso. Prendiamo l'esempio più banale possibile, una divisione: se divido 10 per 2 mi aspetto 5.

@Test
public void test_divide_returnsQuotient() {
    final int actual = calculator.divide(10, 2);
    assertEquals(5, actual);
}

Tutto qui. Nel backend di un prodotto reale gli input sono molto più grandi e gli output più articolati, ma il principio non cambia: dato questo, mi aspetto quello.

Le prove a mano non spariscono: lo sviluppatore chiama i suoi servizi da Postman o clicca sull'interfaccia, e i tester hanno le loro verifiche. Ma questi test hanno una marcia in più, sono automatici: una buona suite di test di unità gira in pochi minuti, e si aggancia alla pipeline che porta il codice sugli ambienti – prima quelli di test e di collaudo, poi la produzione. A ogni modifica e a ogni rilascio riparte tutto da capo, e se un controllo fallisce il rilascio si ferma. Un test si paga una volta, quando lo scrivi; poi lavora gratis per tutta la vita del progetto.

📌 Usciamo dall'astratto: un'automobile

Il codice è una materia astratta, quindi proviamo con qualcosa di tangibile. Un'automobile che si muove è il risultato di tanti componenti che lavorano insieme: motore, frizione, volante, servosterzo, freni, pneumatici.

Prima di mettere la macchina su strada, ogni componente viene provato da solo, al banco di prova. Lo pneumatico ha abbastanza aderenza? La pastiglia del freno regge un certo sforzo senza usurarsi? Questo è un test di unità: verifica un singolo pezzo, isolato da tutto il resto. La strada non c'è: la simula un rullo, e a te interessa solo come reagisce quel pezzo. È veloce, costa poco, e quando fallisce sai esattamente quale pezzo ha un problema.

Poi c'è il giro di prova. Accendi, parti, sterzi, freni: stai verificando che tutti quei componenti, insieme, comunichino tra loro e facciano muovere la macchina. E stavolta la strada è vera. Questo è un test di integrazione: mette alla prova i pezzi dell'applicazione mentre lavorano insieme, e insieme a ciò che sta fuori dall'applicazione ma con cui deve per forza interfacciarsi – un database, un sistema esterno. La strada, appunto: non fa parte della macchina, ma senza strada la macchina non va da nessuna parte. È un test più lento e più costoso, e non ha la granularità di un test di unità: non serve a provare tutte le combinazioni di input che possono passare da un singolo pezzo, ma a verificare che i pezzi, una volta montati, lavorino insieme.

Servono entrambi. Quattro pneumatici perfetti non garantiscono che la macchina sterzi; e un giro di prova andato bene non ti dice quanto dureranno i freni. Per questo in un progetto sano i test di unità sono tanti e quelli di integrazione molti meno: sul modulo principale di DetectION erano circa 1.100 contro 200.

C'è anche una conseguenza sul modo di scrivere il codice. Un pezzo si può provare da solo soltanto se è un pezzo: una funzione che fa dieci cose diverse è difficile da testare quanto un'auto fusa in un unico blocco. Il codice opportunamente stratificato, fatto di funzioni piccole con una singola responsabilità, non è un vezzo da sviluppatori – è ciò che lo rende verificabile.

Una precisazione: io parlo da sviluppatore backend. Sul frontend le due espressioni valgono allo stesso modo, ma il collaudo si complica – l'output non è un valore, è una schermata, e l'input è una persona che clicca dove vuole. Su come si affronta lascio la parola a chi ci lavora tutti i giorni.

📌 Non solo quando va tutto bene

L'errore più comune è testare soltanto il caso in cui tutto funziona – che è anche quello in cui i test servono meno. Ma un freno non si prova solo sull'asciutto a 30 all'ora. Torniamo alla divisione. Dividere per 1 deve restituire il numero di partenza: è un caso limite, e va verificato. Dividere per zero invece non ha un risultato: la funzione deve fallire, e deve farlo nel modo previsto.

@Test
public void test_divide_byZero_throwsException() {
    assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}

Qui il controllo non riguarda un risultato ma un errore: mi aspetto proprio quell'eccezione. Input sbagliati, dati mancanti, sistemi che non rispondono: il mestiere è prevedere l'imprevisto. Che un'eccezione si presenti è normale – l'importante è che non colga il codice di sorpresa.

C'è chi porta questa idea all'estremo e il test lo scrive prima del codice: è il Test Driven Development. È come definire il collaudo del freno prima ancora di costruirlo – da quale velocità deve fermare l'auto, e in quanti metri. Solo dopo si costruisce il pezzo, e si sa che è finito quando supera il collaudo: il vantaggio è che ti costringe a decidere in anticipo non solo cosa vuoi realizzare, ma quali sono i criteri per dire che funziona. Chi scrive requisiti li riconoscerà subito: sono i criteri di accettazione, tradotti in codice.

📌 Quindi, quanti sviluppatori servono?

Dipende, ovviamente. Ma dipende meno da quanto c'è da costruire e più da quanta paura fa toccare quello che esiste già. Ed è qui che i test fanno la differenza:

Per dare un ordine di grandezza: nei moduli backend di DetectION c'erano circa 41.000 righe di codice applicativo e 37.000 di test, quasi un rapporto di uno a uno. Sembra tanto, ed è tanto. È anche il motivo per cui in quasi tre anni si è potuto rimettere mano a qualunque punto del codice senza trattenere il fiato. Attenzione però: i test verificano i casi a cui qualcuno ha pensato, non dimostrano che i bug non esistano. E averne tanti non significa averli buoni – ne avevo parlato nella pillola #23, a proposito dell'illusione della coverage.

La prossima volta che una stima include la voce «test», quindi, non è tempo perso prima della consegna: è ciò che fra un anno ti permetterà di cambiare quel software con tre persone e non con dieci.

Per ora abbiamo solo scaldato i motori: la terza stagione è appena partita! ☕