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
Sfruttare MQTT in cloud e in edge con Azure Event Grid
Load test di ASP.NET Core con k6
Evitare la command injection in un workflow di GitHub
Eseguire attività pianificate con Azure Container Jobs
Creare gruppi di client per Event Grid MQTT
Usare lo spread operator con i collection initializer in C#
Modificare i metadati nell'head dell'HTML di una Blazor Web App
Usare le collection expression per inizializzare una lista di oggetti in C#
Ottimizzare il mapping di liste di tipi semplici con Entity Framework Core
Eseguire una query su SQL Azure tramite un workflow di GitHub
Autenticarsi in modo sicuro su Azure tramite GitHub Actions
Come migrare da una form non tipizzata a una form tipizzata in Angular
I più letti di oggi
- Miglioramenti nelle performance di Angular 16
- Ottimizzare le performance delle collection con le classi FrozenSet e FrozenDictionary
- HTML5 con CSS e JavaScript
- ecco tutte le novità pubblicate sui nostri siti questa settimana: https://aspit.co/wkly buon week-end!
- Ottimizzazione dei block template in Angular 17
- Disabilitare automaticamente un workflow di GitHub (parte 2)