Parlare di unit testing non è mai abbastanza: il suo irrinunciabile valore, spesso mal interpretato come "sforzo", permette a chi crea software di scrivere codice funzionante e documentato. Vediamo come attuare la pratica dello unit testing nelle nostre applicazioni ASP.NET Core.
Iniziamo creando dei nuovi progetti da riga di comando. Avere la padronanza del .NET CLI è importante perché ci permette di gestire i progetti .NET Core in maniera precisa anche su quelle piattaforme in cui la versione Windows di Visual Studio non possa essere installata.
Andremo a creare due progetti:
- MyWebApiApp: un'applicazione ASP.NET Core che espone una Web API;
- MyWebApiApp.Tests: un progetto di unit testing che useremo per verificare che la Web API si comporti secondo le aspettative. In questo esercizio useremo il framework di unit testing MSTest ma teniamo presente che abbiamo a disposizione anche XUnit.
Da una nuova cartella vuota, lanciamo i due comandi di creazione indicando i template da cui iniziare (webapi e mstest), i nomi dei progetti e le loro sottodirectory:
dotnet new webapi --name MyWebApiApp --output src dotnet new mstest --name MyWebApiApp.Tests --output tests
Ora facciamo in modo che il progetto MyWebApiApp.Tests referenzi il progetto MyWebApiApp, condizione necessaria affinché possa testare le sue classi.
dotnet add tests\MyWebApiApp.Tests.csproj reference src\MyWebApiApp.csproj
Infine creiamo una solution che raccolga entrambi i progetti.
dotnet new sln --name MyWebApiApp dotnet sln MyWebApiApp.sln add src/MyWebApiApp.csproj dotnet sln MyWebApiApp.sln add tests/MyWebApiApp.Tests.csproj
Provando ad aprire la solution con Visual Studio, osserviamo che con pochi comandi siamo riusciti a preparare un'applicazione multiprogetto senza ricorrere ad alcuna interfaccia grafica.
Iniziamo dal progetto MyWebApiApp, aprendo il file ValuesController.cs che contiene delle operazioni CRUD dimostrative per gestire un semplice elenco di valori in formato stringa. La nostra prima modifica a questo Controller consiste nell'introdurre una dipendenza dal servizio IValuesRepository, la cui implementazione ci permetterà di leggere e scrivere le stringhe in un database o in un qualsiasi altro tipo di storage persistente.
È sempre preferibile che i servizi applicativi e infrastrutturali siano "iniettati" nel Controller come parametri del costruttore o delle sue action. In questo modo, usando delle interfacce, rendiamo il Controller snello e debolmente accoppiato con gli altri componenti del nostro software, a tutto vantaggio della sua testabilità, come vedremo fra poco.
[Route("api/[controller]")] public class ValuesController : Controller { private readonly IValuesRepository valuesRepository; public ValuesController(IValuesRepository valuesRepository) { this.valuesRepository = valuesRepository; } // GET api/values [HttpGet] public IEnumerable<string> Get() { return valuesRepository.All.Select(v => v.Value).AsEnumerable(); } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return valuesRepository.All.Single(v => v.Id == id).Value; } // POST api/values [HttpPost] public void Post([FromBody]string value) { valuesRepository.Create(value); } // ... altro codice qui ... }
L'interfaccia IValuesRepository è così definita all'interno di un nuovo file Models/IValuesRepository.cs.
public interface IValuesRepository { IQueryable<(int Id, string Value)> All {get; set;} void Create(string value); void Update(int id, string value); void Remove(int id); }
Sarà anche necessario scrivere una classe che implementi IValuesRepository per eseguire materialmente le operazioni di lettura e scrittura nel database. Non essendo importante ai fini di questo script, non verrà illustrata. L'implementazione di servizi e la configurazione del loro ciclo di vita è stata trattata in un precedente script:
https://www.aspitalia.com/script/1230/Gestire-Ciclo-Vita-Servizi-ASP.NET-Core.aspx
Focalizziamoci invece sulla creazione del nostro primo unit test: ci interessa verificare che il Controller stia correttamente usando il servizio IValuesRepository per salvare i valori così da poterli recuperare successivamente.
In ambito di Unit Testing, è abitudine fornire implementazioni fittizie, ad imitazione dei nostri servizi (altrimenti definite mock), che ci permettano di verificare rapidamente che l'interazione tra i componenti stia avvenendo correttamente. Per far questo, usiamo la libreria NSubstitute che installiamo così:
dotnet add tests\MyWebApiApp.Tests.csproj package NSubstitute dotnet restore
Nel progetto MyWebApiApp.Tests, modifichiamo il file UnitTest1.cs come segue.
[TestClass] public class UnitTest1 { //Siamo espressivi con i nomi degli unit test: sono una forma di documentazione [TestMethod] public void GET_action_of_ValuesController_should_return_previously_POSTed_value() { var testValue = "aspitalia"; //Creiamo al volo un'implementazione di IValuesRepository grazie ad NSubstitute var mockValuesRepository = Substitute.For<IValuesRepository>(); //E ora la "configuriamo" impostando solo il comportamento di cui abbiamo bisogno per questo test //In particolare, ci serve tenere un riferimento ai valori inseriti var valueList = new List<(int Id, string Value)>(); //L'inserimento avviene quando il ValuesController invoca il metodo Create mockValuesRepository .When(mock => mock.Create(Arg.Any<string>())) .Do(callInfo => valueList.Add((valueList.Count+1, callInfo.Arg<string>()))); //La proprietà All del repository restituirà la lista mockValuesRepository.All.Returns(valueList.AsQueryable()); //Creiamo un'istanza del controller fornendo il nostro oggetto fittizio var controller = new ValuesController(mockValuesRepository); //Esercitiamo il controller: inseriamo un valore controller.Post(testValue); //e recuperiamo la lista di valori var results = controller.Get(); //Verifichiamo che la lista contenga il valore atteso Assert.AreEqual(1, results.Count()); Assert.AreEqual(testValue, results.First()); //Verifichiamo anche che il metodo Create sia stato invocato mockValuesRepository.Received().Create(testValue); } }
Il nostro primo unit test è completo e non resta che eseguirlo con questo comando:
dotnet test
L'output verde ci confermerà che il comportamento del Controller è conforme alle nostre aspettative.
Gli unit test possono essere usati anche per molteplici altre verifiche, come controllare la robustezza della Web API quando viene invocata senza autorizzazione o con dati non validi. Coprire efficacemente una buona parte del nostro codice è un ottimo modo per ridurre in maniera importante il numero di bug che introduciamo in produzione.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Migliorare i tempi di risposta di GPT tramite lo streaming endpoint in ASP.NET Core
Usare i servizi di Azure OpenAI e ChatGPT in ASP.NET Core con Semantic Kernel
Usare le collection expression per inizializzare una lista di oggetti in C#
Registrare servizi multipli tramite chiavi in ASP.NET Core 8
Effettuare il refresh dei dati di una QuickGrid di Blazor
Cambiare la chiave di partizionamento di Azure Cosmos DB
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
Ordinare randomicamente una lista in C#
Evitare (o ridurre) il repo-jacking sulle GitHub Actions
Eseguire query manipolando le liste contenute in un oggetto mappato verso una colonna JSON
Configurare il nome della run di un workflow di GitHub in base al contesto di esecuzione
Criptare la comunicazione con mTLS in Azure Container Apps
I più letti di oggi
- Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
- ecco tutte le novità pubblicate sui nostri siti questa settimana: https://aspit.co/wkly buon week-end!
- Utilizzare il metodo CountBy di LINQ per semplificare raggruppamenti e i conteggi
- Creare una libreria CSS universale: Cards
- Eseguire script pre e post esecuzione di un workflow di GitHub