Unit testing con ASP.NET Core

di Moreno Gentili, in ASP.NET Core,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi