Quasi tutti gli sviluppatori dicono che l'unit testing è importante. Quasi nessuno lo scrive davvero, almeno non in modo sistematico. La verità è che scrivere test prima del codice sembra una perdita di tempo finché non hai pagato il conto di non averlo fatto.
Il Test-Driven Development ribalta l'ordine: prima scrivi il test (che fallisce), poi il codice che lo fa passare, poi rifattorizzi. Niente magia, solo un loop stretto fra cosa vuoi che faccia e cosa fa davvero. È parte delle metodologie Agili e dell'extreme programming.
L'unit testing puro è verificare la correttezza di singole unità di codice — funzioni o metodi piccoli — in isolamento. Lo scopo è banale: sapere se il pezzo funziona, senza dover lanciare l'intera applicazione.
I test fanno tre cose utili in più: documentano il comportamento atteso (più affidabili di un README che nessuno aggiorna), permettono di rifattorizzare senza paura, e ti dicono quando un cambiamento ha rotto qualcosa che funzionava — al volo, prima di mandare il codice in produzione.
Esempio pratico con xUnit, framework di testing per C#:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
public class CalculatorTests
{
[Fact]
public void Add_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
int a = 5, b = 3;
// Act
var result = calculator.Add(a, b);
// Assert
Assert.Equal(8, result);
}
}Tre fasi: Arrange (prepari), Act (esegui), Assert (verifichi). Test scritti così sono leggibili dopo sei mesi e fanno da documentazione del comportamento atteso.
Il loop del TDD
- Scrivi una lista di casi di test
- Scrivi un test per ogni nuova funzionalità
- Verifica l'esito del test (scrivi codice finché non passa)
- Refactoring del codice
- Eventuale Commit/Deploy
- Ripeti il processo durante tutto lo sviluppo
Quanto costa davvero
Non vendiamoci favole. Scrivere test costa tempo, e all'inizio sembra tempo "perso" rispetto a far partire la nuova feature. Ecco i tre costi reali, in ordine di impatto.
Tempo di scrittura iniziale
Scrivere test che coprano una funzionalità decentemente richiede 30-60% di tempo in più sulla feature stessa. Per un'azienda che misura sulla velocità di consegna, è una pillola difficile da ingoiare. Il payback è dopo 3-6 mesi, quando il primo bug grosso che avresti avuto in produzione lo prendi sul branch in 5 secondi.
Curva di apprendimento
Scrivere test buoni è un mestiere, non un riflesso automatico. La differenza fra un test utile e un test che fa solo finta di testare è l'esperienza. Serve tempo, qualche libro decente (Kent Beck su tutti) e qualche errore.
Codice non testabile
Le applicazioni grandi con molte dipendenze e codice spaghetti sono un incubo da testare. Per renderle testabili serve refactoring strutturale: dependency injection, separazione fra logica e I/O, rottura delle dipendenze cicliche. È la parte di TDD che spaventa di più, perché tocca il codice esistente.
Manutenzione dei test
I test invecchiano insieme al codice. Cambi una signature, devi sistemare 12 test. È un costo continuo che si riduce solo scrivendo test al livello giusto: troppo accoppiati all'implementazione e cambierai test ogni volta, troppo astratti e non testano niente.
Il punto
Bilanciare costi e benefici significa non testare tutto. La copertura 100% è un trofeo, non un obiettivo. Concentrati sulla business logic, sui calcoli, sulle aree dove un bug costa soldi veri. Il resto può vivere senza test, almeno all'inizio.
Cosa testare e cosa no
L'errore più comune è inseguire la code coverage come fosse un KPI. Una copertura del 90% su getter/setter è inutile. Una copertura del 60% sulla business logic critica vale il doppio.
Le cose che vale la pena testare sempre
Calcoli (importi, sconti, IVA), regole di business ("un ordine sopra 100€ ha sconto X"), parser e validatori, tutto ciò che ha edge case (date, fusi orari, valute). Il costo del test è basso e la regressione, quando arriva, costa caro.
Le cose che spesso non conviene testare
Codice glue (controller che chiamano servizi che chiamano repo), wrapper banali, codice che cambia ogni settimana per A/B test. Lì conviene di più un test di integrazione che copre il flusso end-to-end con un test unit per ogni metodo singolo.
Mocking, ma con misura
Il mocking serve a isolare la unità sotto test dalle dipendenze esterne (database, API). Se ti ritrovi a mockare metà del file di test, di solito è il codice che ha troppe dipendenze, non il mock che è sbagliato.
La parte umana, che è quella vera
Il TDD non funziona se solo uno del team lo applica. I test diventano un peso che gli altri ignorano o eliminano alla prima occasione. Funziona quando il team intero lo adotta, il management dà il tempo, e nessuno spedisce codice senza test "perché è solo un fix veloce". È più una questione di cultura che di tecnologia.
CI: i test che girano da soli
I test scritti e mai eseguiti automaticamente valgono poco. Esempio di pipeline GitHub Actions che gira la suite a ogni push:
name: .NET
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build with dotnet
run: dotnet build YourSolution.sln --configuration Release
- name: Run tests
run: dotnet test YourSolution.slnCon questa CI, ogni commit passa la suite prima di poter essere mergiato. È quello che trasforma "test scritti" in "test che proteggono davvero la codebase".
Il TDD non è una religione, è un investimento. Costa di più all'inizio e fa risparmiare molto di più dopo, ma solo se lo applichi dove ha senso e il team ti accompagna.
"Write tests until fear is transformed into boredom" - Kent Beck


