Unit Testing in C#: il banco di collaudo che ti salva il venerdì sera
Scritto da Marco Morello il 10 Agosto 2026
💡 In sintesi: Nessun meccanico sano di mente revisionerebbe una pompa per poi richiudere serbatoio e carene sperando che la moto parta al primo colpo a trecento chilometri da casa. Prima, il pezzo si fissa alla morsa e si mette in pressione. Nel software, invece, spesso assembliamo tutto e testiamo avviando l'intera applicazione. In questo articolo esploriamo come strutturare un banco di collaudo affidabile usando xUnit e il pattern AAA.
Prima di serrare l'ultimo dado sul telaio, il pezzo si fissa alla morsa. Si collegano i tubi di mandata, si attacca il manometro e si mette in pressione il circuito. Se una guarnizione perde o un paraolio cede sotto sforzo, lo scopri lì: su un banco d'acciaio pulito, in cinque minuti, con uno straccio a portata di mano. Non a piedi su un passo di montagna sotto la pioggia battente, né in corsia di sorpasso.
Nel software, invece, molti sviluppatori continuano a montare il motore direttamente sulla strada. Modificano tre righe nella logica di calcolo, premono F5, aspettano che l'applicazione compili, effettuano il login manuale a schermo, navigano attraverso quattro schermate, compilano un form con dati fittizi e guardano se il risultato torna.
Ripetono questo ciclo quindici volte al giorno. Poi rilasciano in produzione il venerdì pomeriggio e passano il fine settimana a rincorrere le eccezioni non gestite sollevate dagli utenti.
I test unitari non sono un esercizio accademico per puristi del codice o una voce da spuntare su una checklist aziendale: sono il banco di collaudo dei singoli componenti prima dell'assemblaggio finale.
1. L'illusione del "Non ho tempo per scrivere test"
L'obiezione più frequente nei team di sviluppo è sempre la stessa: "Dobbiamo consegnare la feature entro fine sprint, non abbiamo tempo per scrivere codice che testa altro codice".
È lo stesso ragionamento di chi dice di non avere tempo per controllare la pressione degli pneumatici prima di un viaggio di tremila chilometri perché deve partire subito. Finisci per fermarti ogni cinquanta chilometri a gonfiare la gomma a mano, chiedendoti perché il viaggio duri il doppio del previsto.
Testare manualmente avviando l'intera applicazione comporta costi occulti enormi:
- Lentezza del ciclo di feedback: Compilare l'intero stack, avviare il web server, caricare l'interfaccia e ricreare lo stato dei dati richiede minuti. Un test unitario gira in millisecondi.
- Effetti collaterali invisibili: Quando tocchi una classe per correggere un bug, non puoi sapere a occhio nudo se hai rotto un comportamento introdotto sei mesi fa su un flusso correlato.
- Mancanza di regressione automatica: Se scopri un difetto oggi e lo sistemi senza un test a presidio, quello stesso bug si ripresenterà non appena un collega rimetterà mano a quel file.
L'evoluzione del costo di un bug nel ciclo di vita del software
Scrivere unit test significa spostare il rilevamento del guasto a monte (Shift-Left), quando risolverlo costa pochi secondi e zero frustrazione.
2. Il Pattern AAA: Come si allestisce la prova
In xUnit (il framework di test standard de facto nell'ecosistema moderno di .NET), un test unitario ben strutturato segue una regola ferrea: il pattern AAA (Arrange, Act, Assert).
Se hai familiarità con un banco di flussaggio o una prova di trazione meccanica, il parallelo è millimetrico:
Morsa e Parametri iniziali
Esecuzione della sollecitazione
Misura reale vs Tolleranza
Immaginiamo di dover collaudare una logica di officina reale: un componente che calcola la correzione della coppia di serraggio quando si utilizza una prolunga sulla chiave dinamometrica. La formula fisica richiede che la coppia impostata sulla scala della chiave sia inferiore a quella effettiva richiesta sul bullone, in base alla lunghezza della prolunga.
Vediamo la classe di dominio pulita, isolata da qualsiasi database o interfaccia grafica:
namespace Officina.Core.Meccanica;
public class CalcolatoreDinamometrica
{
public decimal CalcolaCoppiaScala(decimal coppiaTargetNm, decimal lunghezzaChiaveMm, decimal prolungaMm)
{
if (coppiaTargetNm <= 0)
throw new ArgumentOutOfRangeException(nameof(coppiaTargetNm), "La coppia target deve essere positiva.");
if (lunghezzaChiaveMm <= 0)
throw new ArgumentOutOfRangeException(nameof(lunghezzaChiaveMm), "La lunghezza della chiave deve essere positiva.");
if (prolungaMm < 0)
throw new ArgumentOutOfRangeException(nameof(prolungaMm), "La prolunga non può avere misura negativa.");
if (prolungaMm == 0)
return coppiaTargetNm;
decimal rapporto = lunghezzaChiaveMm / (lunghezzaChiaveMm + prolungaMm);
return Math.Round(coppiaTargetNm * rapporto, 2);
}
}
Ed ecco il relativo banco prova scritto con xUnit sfruttando l'attributo [Fact], che identifica un test a scenario singolo:
namespace Officina.Core.Tests;
using Xunit;
using Officina.Core.Meccanica;
public class CalcolatoreDinamometricaTests
{
[Fact]
public void CalcolaCoppiaScala_ConProlungaValida_DeveRidurreCoppiaImpostataCorrettamente()
{
// 1. Arrange (Allestimento del banco)
var calcolatore = new CalcolatoreDinamometrica();
decimal coppiaTarget = 100m;
decimal lunghezzaChiave = 400m;
decimal lunghezzaProlunga = 100m;
decimal valoreAtteso = 80.00m; // 100 * (400 / 500) = 80
// 2. Act (Esecuzione della misura)
decimal risultatoReale = calcolatore.CalcolaCoppiaScala(coppiaTarget, lunghezzaChiave, lunghezzaProlunga);
// 3. Assert (Verifica tolleranza)
Assert.Equal(valoreAtteso, risultatoReale);
}
[Fact]
public void CalcolaCoppiaScala_SenzaProlunga_DeveRestituireLaCoppiaTargetInalterata()
{
// Arrange
var calcolatore = new CalcolatoreDinamometrica();
decimal coppiaTarget = 50m;
decimal lunghezzaChiave = 300m;
decimal prolungaNulla = 0m;
// Act
decimal risultatoReale = calcolatore.CalcolaCoppiaScala(coppiaTarget, lunghezzaChiave, prolungaNulla);
// Assert
Assert.Equal(coppiaTarget, risultatoReale);
}
[Fact]
public void CalcolaCoppiaScala_ConCoppiaTargetNegativa_DeveLanciareArgumentOutOfRangeException()
{
// Arrange
var calcolatore = new CalcolatoreDinamometrica();
// Act & Assert (Verifica della sicurezza intrinseca per scenari di errore)
Assert.Throws<ArgumentOutOfRangeException>(() =>
calcolatore.CalcolaCoppiaScala(-10m, 400m, 50m));
}
}
La convenzione dei nomi è esplicita: Metodo_Condizione_RisultatoAtteso. Chiunque legga il report del test sa esattamente cosa si è rotto senza dover aprire il file sorgente.
3. [Theory] e [InlineData]: Il Collaudo Parametrico
Nella produzione reale non provi un solo pezzo a una sola temperatura. Vuoi vedere come risponde il componente al minimo, a regime nominale e al limite di snervamento.
Copiare e incollare lo stesso metodo di test dieci volte cambiando solo due numeri genera codice spazzatura, difficile da mantenere. Per questo xUnit mette a disposizione l'attributo [Theory] combinato con [InlineData]:
public class CollaudoIdraulicoTests
{
[Theory]
[InlineData(10, 100, 90.0)] // Regime normale: 10 cicli = 90 bar residui
[InlineData(50, 100, 50.0)] // Mezza vita: 50 cicli = 50 bar residui
[InlineData(90, 100, 10.0)] // Fine vita: 90 cicli = 10 bar residui
[InlineData(100, 100, 0.0)] // Esaurimento totale: 0 bar
public void CalcolaPressioneResidua_DatiCicliLavoro_RestituiscePressioneCorretta(
int cicliEseguiti,
decimal pressioneInizialeBar,
decimal pressioneAttesaBar)
{
// Arrange
var circuito = new CircuitoPneumatico();
// Act
decimal pressioneReale = circuito.CalcolaPressioneResidua(cicliEseguiti, pressioneInizialeBar);
// Assert
Assert.Equal(pressioneAttesaBar, pressioneReale);
}
}
Con una sola dichiarazione, xUnit esegue quattro collaudi distinti nel terminale. Se la formula fallisce soltanto per il caso limite a 100 cicli, il runner isola esattamente la riga incriminata senza bloccare l'ispezione delle altre.
4. Cosa collaudare (e cosa lasciare stare)
Uno degli errori tipici di chi inizia con gli unit test è voler testare qualsiasi riga di codice:
-
Proprietà banali
Evita di testare getter e setter semplici come
public string Modello { get; set; }. Non c'è logica. Staresti testando il compilatore C#, che è già stato ampiamente testato da Microsoft. -
Librerie esterne e Framework
Non devi verificare se
List.Add()inserisce effettivamente un elemento in memoria, o se Entity Framework salva un record su disco. Testa il tuo codice, non gli strumenti consolidati. -
Dettagli implementativi privati
Testa sempre il contratto pubblico della classe (cosa entra e cosa esce). Se in futuro ottimizzi un algoritmo privato interno mantenendo invariato il risultato finale, i test dovranno continuare a passare senza necessitare di aggiornamenti.
if/else complessi), sulle eccezioni di guardia (valori nulli) e sui valori limite matematici (zero, negativi, overflow). Testa lì dove un errore costa ore di fermo macchina o frustrazione per l'utente.
5. Il Banco Prova: Metti alla prova la suite xUnit
Per toccare con mano la differenza tra un collaudo superato e una rottura a banco, usa il simulatore sottostante. Il codice simula il calcolo del decadimento di pressione idraulica in base ai cicli macchina.
Conclusioni: L'abitudine che fa la differenza
Imparare a scrivere test unitari all'inizio richiede uno sforzo cosciente: ti costringe a scrivere classi disaccoppiate, a rispettare il principio di singola responsabilità (SRP) e a evitare metodi monolitici da cinquecento righe impossibili da isolare.
Tuttavia, una volta acquisito l'automatismo, il ritorno sull'investimento è immediato:
- Refattorizzi il codice senza paura, perché hai una cintura di sicurezza che ti avverte al minimo passo falso.
- Non perdi tempo a rieseguire flussi manuali infiniti a ogni modifica.
- Dormi sonni tranquilli la notte dopo una release, perché i tuoi componenti sono stati collaudati a banco singolarmente prima di essere spediti al cliente.