Le strategie di testing software vengono quasi sempre descritte in termini di copertura, e la copertura è il numero meno informativo dell’intera disciplina. Una codebase al novanta per cento può rilasciare un bug proprio sul percorso più usato, perché la copertura misura quali righe sono state eseguite durante un giro di test, non se su quelle righe sia stato verificato qualcosa di significativo.
I team che si fidano della propria suite non sono quelli con la percentuale più alta. Sono quelli i cui test falliscono quando qualcosa è davvero rotto e restano silenziosi il resto del tempo, una proprietà che si rivela molto più difficile da comprare.
La domanda che vale la pena porsi su qualunque test: se questo fallisce, saprò cosa fare? Un test che fallisce perché il comportamento è cambiato ti dice qualcosa. Un test che fallisce perché si è spostato un dettaglio implementativo ti dice solo che qualcuno ha rifattorizzato, e dopo un numero sufficiente di casi così il team smette di leggere i fallimenti e comincia a rilanciare la pipeline finché non diventa verde.
Perché la copertura inganna
La copertura risponde alla domanda se quella riga sia stata eseguita, che non è la stessa cosa della domanda se quella riga sia corretta. Un test che chiama una funzione e non verifica nulla produce esattamente la stessa copertura di uno che controlla ogni ramo del risultato.
Questa distanza ha conseguenze pratiche. Obiettivi fissati su una percentuale precisa producono in modo affidabile test scritti per raggiungere il numero: test esaustivi su getter banali, niente sul percorso di pagamento dove i rami sono faticosi da preparare. Il numero sale, il rischio non si muove.
La copertura è utile in una sola direzione. Una copertura bassa in un’area critica è un segnale reale su cui vale la pena agire. Una copertura complessiva alta non dimostra nulla, e trattarla come obiettivo invece che come diagnosi è esattamente il modo in cui un team finisce con migliaia di test e nessuna fiducia.
Strategie di testing software: quali test ripagano il loro costo
Ogni test è tanto una passività quanto un asset. Va mantenuto, rallenta la suite e ogni tanto sbaglierà. La domanda utile è quali di essi ripagano tutto questo.
I test unitari ripagano quando la logica è davvero complessa e indipendente dall’infrastruttura: regole di prezzo, gestione delle date, controlli di permessi, parser. Veloci, precisi, e sopravvivono al refactoring perché il comportamento che descrivono è reale.
I test di integrazione ripagano molto più di quanto la maggior parte dei team si aspetti, perché la maggioranza dei bug di produzione vive ai confini e non dentro le funzioni. La query che funziona contro un mock e fallisce contro il database vero. L’API il cui campo opzionale nella pratica non arriva mai. Questi test sono più lenti e ne vale la pena.
I test end-to-end ripagano solo per una manciata di percorsi, e il loro numero dovrebbe restare abbastanza piccolo da poterli elencare a voce. Registrarsi, comprare, l’unica cosa che la tua azienda fa davvero. Sono lenti, fragili e costosi, e una suite di duecento test di questo tipo è la principale fonte di sofferenza di un team.
La forma verso cui converge la maggior parte delle codebase è fatta di molti test unitari, di uno strato solido di test di integrazione e di una manciata di percorsi end-to-end, cioè la piramide dei test descritta da Martin Fowler. Dove i team sbagliano è di solito lo strato di mezzo: hanno test unitari e test end-to-end e quasi niente che verifichi che i pezzi combacino.
I test instabili sono un problema di fiducia
Un test che fallisce una volta su venti esecuzioni è peggio di nessun test, e la ragione è comportamentale prima che tecnica.
Non appena una suite contiene qualche test ballerino, il team impara che il rosso non significa necessariamente rotto. Rilanciare diventa routine. Poi viene rilanciato anche un fallimento vero, e quando passa al terzo tentativo qualcuno lo integra. La suite ha smesso di funzionare come segnale pur continuando a consumare tempo.
Tratta l’instabilità come un difetto con la stessa priorità di un bug in produzione. Metti subito il test in quarantena così che la pipeline diventi verde in modo onesto, poi correggilo o cancellalo. Le cause abituali sono lo stato condiviso fra test, le dipendenze reali dal tempo e il fare affidamento su un ordine di esecuzione che il runner non garantisce.
Cancellare un test instabile è un esito legittimo. Un test di cui nessuno si fida non offre alcuna protezione, e rimuoverlo almeno smette di consumare attenzione.
Testare il comportamento, non l’implementazione
La causa più comune delle suite di test costose sono i test accoppiati al modo in cui il codice funziona invece che a ciò che fa.
Simulare ogni dipendenza e verificare che un metodo preciso sia stato chiamato con argomenti precisi produce un test che fallisce a ogni refactoring, che il comportamento sia cambiato o no. È esattamente il contrario di quello che serve: il refactoring è il momento in cui vuoi di più che la suite ti dica che non si è rotto nulla, e invece produce cinquanta fallimenti da smaltire a mano.
L’alternativa è verificare i risultati. Dato questo input, il sistema produce questo output oppure raggiunge questo stato. Test così sopravvivono alle riscritture interne, il che significa che continuano a proteggerti esattamente durante i cambiamenti che portano più rischio.
I mock si guadagnano il loro posto ai confini veri: un fornitore di pagamenti, un servizio di posta, qualunque cosa lenta o con effetti collaterali che non puoi avere durante un giro di test. Dentro il tuo codice costano di solito più di quanto restituiscano.
Farla girare in CI
Una suite che nessuno aspetta è una suite che viene saltata. Se l’esecuzione completa richiede quaranta minuti, la gente fa push e passa oltre, e il riscontro arriva quando hanno già cominciato altro.
Dividila. Test unitari e di integrazione veloci a ogni push, con una risposta in un paio di minuti. I lenti percorsi end-to-end al merge oppure su pianificazione. È lo stesso ragionamento della disciplina di rilascio descritta nella nostra guida alle best practice CI/CD per i team britannici nel 2026 .
Rendi leggibili i fallimenti. Un fallimento che dice solo che un’asserzione era falsa, senza alcuna indicazione di cosa stesse controllando, costa dieci minuti di archeologia ogni volta. Nominare i test in base al comportamento che proteggono trasforma la lista dei fallimenti in una descrizione di ciò che si è rotto.
E mantieni la suite deterministica. Nessuna chiamata di rete reale, nessuna dipendenza dalla data di oggi senza controllarla, nessuna assunzione sull’ordine. Ogni test non deterministico è un test ballerino futuro.
Da dove partire su una codebase senza test
Non provare a ottenere la copertura completa a posteriori; lo sforzo è enorme e gran parte protegge codice che nessuno tocca.
Parti dai percorsi dove un bug costa denaro e scrivi prima test di integrazione attorno a essi, perché sono quelli che intercettano di più per ogni test scritto. Poi aggiungi un test a ogni correzione di bug, riproducendo il fallimento prima di ripararlo. Così la copertura cresce esattamente dove i difetti si verificano davvero, che è il miglior segnale disponibile su dove vive il rischio.
Mecanik rivede e costruisce strategie di test come parte del nostro lavoro di sviluppo software su misura , partendo di solito dalla domanda su quali fallimenti farebbero davvero male. Se la tua suite è grande e il tuo team rilascia ancora con ansia, il problema raramente è il numero di test.
Da leggere anche: Documentazione tecnica che viene davvero letta , Onboarding degli sviluppatori che produce nella prima settimana , Postmortem che cambiano davvero qualcosa e Versionamento delle API: quando rompere e come evitarlo .
Domande frequenti
Una copertura di test alta è un buon obiettivo? Non da sola. La copertura misura quali righe sono state eseguite durante un giro di test, non se su quelle righe sia stato verificato qualcosa di significativo, quindi un test che chiama una funzione e non verifica nulla ottiene lo stesso punteggio di uno che controlla ogni ramo. Una copertura bassa su un percorso critico è un segnale utile; una percentuale complessiva alta non dimostra granché.
Qual è il giusto mix di test unitari, di integrazione ed end-to-end? Molti test unitari per la logica davvero complessa, uno strato consistente di test di integrazione perché quasi tutti i bug di produzione vivono ai confini, e un piccolo numero di percorsi end-to-end che puoi elencare a voce. La maggior parte dei team sbaglia lo strato di mezzo: ha test unitari ed end-to-end ma poco che verifichi che i pezzi combacino.
Come dovrei gestire i test instabili? Trattali come difetti con la priorità di un bug in produzione. Metti subito il test in quarantena così che la pipeline resti onesta, poi correggilo o cancellalo. Non appena una suite ha qualche test ballerino il team impara che il rosso non significa rotto, rilancia per riflesso e prima o poi integra un fallimento vero. Cancellare un test instabile è un esito legittimo.
Dovrei simulare le dipendenze nei test? Ai confini veri sì: fornitori di pagamento, servizi di posta, qualunque cosa lenta o con effetti collaterali. Dentro il tuo codice i mock costano di solito più di quanto restituiscano, perché verificare che un metodo preciso sia stato chiamato con argomenti precisi fa fallire il test a ogni refactoring, che il comportamento sia cambiato o no.
Come aggiungo test a una codebase che non ne ha? Non provare a ottenere la copertura completa a posteriori. Scrivi test di integrazione attorno ai percorsi dove un bug costa denaro, dato che sono quelli che intercettano di più per ogni test scritto. Poi aggiungi un test a ogni correzione di bug, riproducendo il fallimento prima di ripararlo, così la copertura cresce esattamente dove i difetti si verificano davvero.
Commenti