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
Limitare le richieste lato server con l'interactive routing di Blazor 8
Referenziare un @layer più alto in CSS
Disabilitare automaticamente un workflow di GitHub (parte 2)
Gestire eccezioni nei plugin di Semantic Kernel in ASP.NET Core Web API
Evitare (o ridurre) il repo-jacking sulle GitHub Actions
Sfruttare al massimo i topic space di Event Grid MQTT
Creare alias per tipi generici e tuple in C#
Modificare i metadati nell'head dell'HTML di una Blazor Web App
Esporre i propri servizi applicativi con Semantic Kernel e ASP.NET Web API
Gestire domini wildcard in Azure Container Apps
Ottimizzare il mapping di liste di tipi semplici con Entity Framework Core
Migliorare la sicurezza dei prompt con Azure AI Studio