Torniamo anche questa settimana a parlare di unit testing con ASP.NET Core. Nel precedente script (https://www.aspitalia.com/script/1272/Unit-Testing-ASP.NET-Core.aspx) abbiamo verificato che un'action interagisse correttamente con un repository per l'inserimento di un valore in una lista. Quest'oggi continuiamo il testing della stessa action per verificare il suo comportamento in presenza di dati non validi.
Iniziamo modificando l'action in modo che il parametro sia validato prima del suo effettivo inserimento. In questo esempio, l'action restituirà una risposta con status code 400 (Bad Request) per valori vuoti o null.
// POST api/values [HttpPost] public void Post([FromBody]string value) { if (string.IsNullOrEmpty(value)) { HttpContext.Response.StatusCode = 400; //Bad Request return; } valuesRepository.Create(value); }
Poi scriviamo un test per verificare che l'action stia effettivamente restituendo lo status code 400 (Bad Request) quando le forniamo un valore non valido. In ambito di unit testing non è necessario inviare una vera richiesta HTTP perché possiamo creare un oggetto di tipo DefaultHttpContext e manipolare il suo stato secondo le nostre esigenze. In questo modo, potremo verificare il comportamento dell'action proprio come se fosse stata invocata da una vera richiesta HTTP. Il vantaggio di usare questa tecnica diventa più che tangibile quando la suite arriva a contenere migliaia di test. Grazie ad ASP.NET Core, testare i Controller diventa molto semplice e rapido nell'esecuzione.
[TestMethod] public void POST_with_invalid_data_should_return_a_400_bad_request_response() { //Proviamo con un valore non valido come una stringa vuota... var testValue = ""; //...e ci aspettiamo che produca una risposta con status code 400 var expectedStatusCode = 400; //Creiamo un repository fittizio var mockValuesRepository = Substitute.For<IValuesRepository>(); //Creiamo l'istanza del controller di ASP.NET Core var controller = new ValuesController(mockValuesRepository); //Creiamo un contesto per il Controller (ci evita di dover inviare richieste HTTP) var httpContext = new DefaultHttpContext(); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; //Esercitiamo il controller provando ad inserire il valore controller.Post(testValue); //Dato che si tratta di un valore non valido, ci aspettiamo che //l'action restituisca uno status code 400 (Bad Request) Assert.AreEqual(expectedStatusCode, httpContext.Response.StatusCode); }
Ora scriviamo un secondo test per verificare che lo status code sia 200 (OK) quando viene fornito un valore valido. Possiamo procedere in due modi:
- Duplicare il codice del test precedente e modificare il valore delle variabili testValue ed expectedStatusCode;
- Oppure mantenere un unico test, modificandolo affinché sia in grado di verificare entrambi i casi.
Se scegliamo la seconda opzione, allora dovremo rendere il test parametrico e sfruttare l'attributo [DataRow] per fornirgli un set di argomenti su cui lavorare. Il test verrà eseguito un certo numero di volte, in base alle occorrenze di tale attributo. Questo approccio è così conveniente che ci permette di aggiungere col minimo sforzo tutti i casi limite trattati nella specifica, come ad esempio valori null, stringhe composte di soli spazi, e così via. Modifichiamo quindi il test in questo modo, così che accetti il testValue e l'expectedStatusCode come parametri.
[TestMethod] [DataRow("", 400)] [DataRow(null, 400)] [DataRow(" ", 400)] [DataRow("Foo", 200)] public void POST_should_return_an_appropriate_status_code(string testValue, int expectedStatusCode) { //Il test accetta i due parametri testValue ed expectedStatusCode //Verrà eseguito 4 volte, perché è stato decorato con 4 attributi [DataRow] var mockValuesRepository = Substitute.For<IValuesRepository>(); var controller = new ValuesController(mockValuesRepository); var httpContext = new DefaultHttpContext(); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; controller.Post(testValue); Assert.AreEqual(expectedStatusCode, httpContext.Response.StatusCode); }
Eseguendo il comando dotnet test, il .NET Core CLI manderà in esecuzione il test fornendo, di volta in volta, i vari set di argomenti indicati negli attributi DataRow. Nel caso in cui il test dovesse fallire anche per un singolo set di argomenti, allora l'intero test fallirà e ne avremo evidenza nell'output del comando.
Come si vede in figura, abbiamo una chiara indicazione che il test sia fallito a causa degli argomenti " " e 400. Grazie al test, abbiamo dunque scoperto di non aver gestito il caso di stringhe composte di soli spazi nella nostra implementazione. Correggiamo questo potenziale bug come segue, e poi eseguiamo di nuovo il test per vederlo passare.
// POST api/values [HttpPost] public void Post([FromBody]string value) { //Modificato qui: ora anche le stringhe con soli spazi sono considerate non valide if (string.IsNullOrWhiteSpace(value)) { HttpContext.Response.StatusCode = 400; //Bad Request return; } valuesRepository.Create(value); }
Lo unit testing è una pratica decisamente utile, il cui valore risulta evidente sperimentandola. In alcune situazioni, potremmo decidere di adottare il Test-driven development, un particolare approccio alla scrittura del software che ci porta innanzitutto a creare un test, vederlo fallire, e soltanto a quel punto scrivere la minima quantità di codice necessaria a farlo passare. In questo modo andiamo ad implementare metodicamente tutti i punti di una specifica, uno dopo l'altro, ragionando per gradi e senza farci sopraffare dalla complessità di ciò che si deve realizzare.
Periodicamente, quando i testi hanno tutti successo, possiamo procedere con un'attività di refactoring che semplificherà ulteriormente il codice e lo renderà più manutenibile. Si tratta di un'attività da svolgere con serenità perché, in qualsiasi momento, possiamo rieseguire l'intera suite di test per verificare che le modifiche apportate al codice non abbiano avuto impatti negativi.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Eseguire query per recuperare il padre di un record che sfrutta il tipo HierarchyID in Entity Framework
Hosting di componenti WebAssembly in un'applicazione Blazor static
Creare una libreria CSS universale: Clip-path
Utilizzare Model as a Service su Microsoft Azure
Come migrare da una form non tipizzata a una form tipizzata in Angular
Eseguire operazioni sui blob con Azure Storage Actions
Eseguire script pre e post esecuzione di un workflow di GitHub
Ordine e importanza per @layer in CSS
Inference di dati strutturati da testo con Semantic Kernel e ASP.NET Core Web API
.NET Conference Italia 2024
Utilizzare QuickGrid di Blazor con Entity Framework
Come EF 8 ha ottimizzato le query che usano il metodo Contains