Abilitare unit test automatici

di Microsoft

Scarica il PDF

Questo è il passaggio 12 di un'esercitazione gratuita sull'applicazione "NerdDinner" che illustra come creare un'applicazione Web di piccole dimensioni, ma completa, usando ASP.NET MVC 1.

Il passaggio 12 illustra come sviluppare una suite di unit test automatizzati che verificano la funzionalità NerdDinner e che consentiranno di apportare modifiche e miglioramenti all'applicazione in futuro.

Se si usa ASP.NET MVC 3, è consigliabile seguire le esercitazioni Introduzione Con MVC 3 o MVC Music Store.

NerdDinner Passaggio 12: Unit Testing

Verrà ora sviluppata una suite di unit test automatizzati che verificano la funzionalità NerdDinner e che consentiranno di apportare modifiche e miglioramenti all'applicazione in futuro.

Perché unit test?

Sulla guida al lavoro una mattina si ha un improvviso lampo di ispirazione su un'applicazione su cui si sta lavorando. Ci si rende conto che è possibile implementare una modifica che renderà l'applicazione notevolmente migliore. Potrebbe trattarsi di un refactoring che pulisce il codice, aggiunge una nuova funzionalità o corregge un bug.

La domanda che ti confronta quando arrivi al tuo computer è : "quanto è sicuro fare questo miglioramento?" Cosa succede se fare il cambiamento ha effetti collaterali o rompe qualcosa? La modifica potrebbe essere semplice e l'implementazione richiede solo alcuni minuti, ma cosa accade se sono necessarie ore per testare manualmente tutti gli scenari dell'applicazione? Cosa accade se si dimentica di coprire uno scenario e un'applicazione interrotta entra in produzione? Questo miglioramento vale davvero la pena di fare tutto il lavoro?

Gli unit test automatizzati possono fornire una rete di sicurezza che consente di migliorare continuamente le applicazioni ed evitare di avere paura del codice su cui si sta lavorando. La presenza di test automatizzati che verificano rapidamente la funzionalità consente di scrivere codice con sicurezza e di apportare miglioramenti che altrimenti non si sono sentiti a proprio agio. Consentono inoltre di creare soluzioni più gestibili e con una durata più lunga, con un ritorno molto più elevato sugli investimenti.

Il framework MVC ASP.NET semplifica e naturale la funzionalità dell'applicazione di unit test. Abilita anche un flusso di lavoro di sviluppo basato su test (TDD) che consente lo sviluppo basato su test.

Progetto NerdDinner.Tests

Quando è stata creata l'applicazione NerdDinner all'inizio di questa esercitazione, viene visualizzata una finestra di dialogo che chiede se si vuole creare un progetto di unit test per procedere con il progetto dell'applicazione:

Screenshot della finestra di dialogo Crea progetto unit test. Sì, è selezionata l'opzione Crea un progetto di unit test. Nerd Dinner dot Tests viene scritto come nome del progetto test.

È stato selezionato il pulsante di opzione "Sì, creare un progetto di unit test", che ha comportato l'aggiunta di un progetto "NerdDinner.Tests" alla soluzione:

Screenshot dell'albero di spostamento Esplora soluzioni. È selezionata l'opzione Test punto cena nerd.

Il progetto NerdDinner.Tests fa riferimento all'assembly di progetto dell'applicazione NerdDinner e consente di aggiungere facilmente test automatizzati per verificare la funzionalità dell'applicazione.

Creazione di unit test per la classe del modello Dinner

Aggiungere alcuni test al progetto NerdDinner.Tests che verificano la classe Dinner creata al momento della compilazione del livello del modello.

Si inizierà creando una nuova cartella all'interno del progetto di test denominato "Models" in cui verranno inseriti i test correlati al modello. Fare clic con il pulsante destro del mouse sulla cartella e scegliere il comando di menu Aggiungi nuovo> test . Verrà visualizzata la finestra di dialogo "Aggiungi nuovo test".

Si sceglierà di creare uno "Unit Test" e denominarlo "DinnerTest.cs":

Screenshot della finestra di dialogo Aggiungi nuovo test. Unit Test è evidenziato. Dinner Test dot c s viene scritto come Nome test.

Quando si fa clic sul pulsante "ok" Visual Studio aggiunge (e apre) un file DinnerTest.cs al progetto:

Screenshot del file Dinner Test dot c s in Visual Studio.

Il modello di unit test di Visual Studio predefinito include un gruppo di codice boiler-plate all'interno di esso che trovo un po 'disordinato. Eseguire la pulizia in modo da contenere solo il codice seguente:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

L'attributo [TestClass] nella classe DinnerTest precedente lo identifica come classe che conterrà test, nonché l'inizializzazione dei test e il codice di disinstallazione facoltativi. È possibile definire i test all'interno di esso aggiungendo metodi pubblici con un attributo [TestMethod] su di essi.

Di seguito sono riportati i primi due test che verranno aggiunti per l'esercizio della classe Dinner. Il primo test verifica che la cena non sia valida se viene creata una nuova cena senza che tutte le proprietà siano impostate correttamente. Il secondo test verifica che la cena sia valida quando una cena ha tutte le proprietà impostate con valori validi:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Si noterà sopra che i nomi dei test sono molto espliciti (e in qualche modo dettagliato). Questa operazione viene eseguita perché si potrebbero creare centinaia o migliaia di piccoli test e si vuole semplificare la determinazione rapida dell'intento e del comportamento di ognuno di essi (soprattutto quando si esamina un elenco di errori in un test runner). I nomi dei test devono essere denominati dopo la funzionalità che stanno testando. In precedenza viene usato un modello di denominazione "Noun_Should_Verb".

Stiamo strutturando i test usando il modello di test "AAA", che è l'acronimo di "Arrange, Act, Assert":

  • Disponi: configurare l'unità sottoposta a test
  • Act: esercizio dell'unità di test e acquisizione dei risultati
  • Assert: verificare il comportamento

Quando si scrivono test, è consigliabile evitare che i singoli test vengano eseguiti troppo. Ogni test deve invece verificare solo un singolo concetto (che rende molto più semplice individuare la causa degli errori). Una buona linea guida consiste nel provare e avere una singola istruzione assert per ogni test. Se si dispone di più di un'istruzione assert in un metodo di test, assicurarsi che vengano tutti usati per testare lo stesso concetto. In caso di dubbio, effettuare un altro test.

Esecuzione test

Visual Studio 2008 Professional (e versioni successive) include un test runner predefinito che può essere usato per eseguire progetti unit test di Visual Studio all'interno dell'IDE. È possibile selezionare il comando di menu Test-Run-All Tests (Test-Run-All>> test) (o premere CTRL R, A) per eseguire tutti gli unit test. In alternativa, è possibile posizionare il cursore all'interno di una classe di test o di un metodo di test specifico e usare il comando di menu Test-Run-Tests>> nel menu di scelta rapida corrente (o digitare CTRL R, T) per eseguire un subset degli unit test.

Posizionare il cursore all'interno della classe DinnerTest e digitare "CTRL R, T" per eseguire i due test appena definiti. Quando si esegue questa operazione, verrà visualizzata una finestra "Risultati test" all'interno di Visual Studio e verranno visualizzati i risultati dell'esecuzione del test elencata al suo interno:

Screenshot della finestra Risultati test in Visual Studio. I risultati dell'esecuzione del test sono elencati all'interno.

Nota: la finestra dei risultati del test di Visual Studio non visualizza la colonna Nome classe per impostazione predefinita. È possibile aggiungerlo facendo clic con il pulsante destro del mouse nella finestra Risultati test e usando il comando di menu Aggiungi/Rimuovi colonne.

I due test hanno impiegato solo una frazione di secondo per l'esecuzione e, come si può vedere, hanno superato entrambi. È ora possibile aggiungerli e aumentarli creando test aggiuntivi che verificano le convalide specifiche delle regole, nonché i due metodi helper, Ovvero IsUserHost() e IsUserRegistered() aggiunti alla classe Dinner. Avere tutti questi test sul posto per la classe Dinner renderà molto più semplice e sicuro aggiungere nuove regole di business e convalide in futuro. È possibile aggiungere la nuova logica della regola a Dinner e quindi in pochi secondi verificare che non sia stata interrotta alcuna funzionalità logica precedente.

Si noti che l'uso di un nome di test descrittivo semplifica la comprensione rapida di ciò che ogni test verifica. È consigliabile usare il comando di menu Strumenti-Opzioni>, aprire la schermata di configurazione Strumenti di test-Test> Execution e selezionare la casella di controllo "Fare doppio clic su un risultato di unit test non riuscito o inconclusivo visualizza il punto di errore nel test". In questo modo sarà possibile fare doppio clic su un errore nella finestra dei risultati del test e passare immediatamente all'esito negativo dell'asserzione.

Creazione di unit test di DinnersController

Verranno ora creati alcuni unit test che verificano la funzionalità DinnersController. Si inizierà facendo clic con il pulsante destro del mouse sulla cartella "Controller" all'interno del progetto test e quindi scegliere il comando di menu Aggiungi nuovo> test . Verrà creato uno "Unit Test" e denominato "DinnersControllerTest.cs".

Verranno creati due metodi di test che verificano il metodo di azione Details() in DinnersController. Il primo verificherà che venga restituita una visualizzazione quando viene richiesta una cena esistente. Il secondo verificherà che venga restituita una visualizzazione "NotFound" quando viene richiesta una cena inesistente:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

Il codice precedente viene compilato correttamente. Quando si eseguono i test, tuttavia, entrambi hanno esito negativo:

Screenshot del codice. Entrambi i test non sono riusciti.

Se si esaminano i messaggi di errore, si noterà che il motivo per cui i test non sono riusciti perché la classe DinnersRepository non è riuscita a connettersi a un database. L'applicazione NerdDinner usa una stringa di connessione a un file di SQL Server Express locale che si trova nella directory \App_Data del progetto di applicazione NerdDinner. Poiché il progetto NerdDinner.Tests compila ed esegue in una directory diversa, il percorso relativo della stringa di connessione non è corretto.

È possibile risolvere il problema copiando il file di database SQL Express nel progetto di test e quindi aggiungendo una stringa di connessione di test appropriata nel App.config del progetto di test. In questo modo si otterrebbero i test precedenti sbloccati e in esecuzione.

Tuttavia, il codice di unit test che usa un database reale comporta numerose sfide. In particolare:

  • Rallenta significativamente il tempo di esecuzione degli unit test. Più tempo è necessario eseguire i test, meno è probabile che vengano eseguiti frequentemente. Idealmente, si vuole che gli unit test siano in grado di essere eseguiti in pochi secondi e che si tratti di operazioni che si eseguono naturalmente durante la compilazione del progetto.
  • Complica la logica di installazione e pulizia all'interno dei test. Si vuole che ogni unit test sia isolato e indipendente da altri (senza effetti collaterali o dipendenze). Quando si lavora su un database reale, è necessario tenere presente lo stato e reimpostarlo tra i test.

Si esamini ora un modello di progettazione denominato "inserimento delle dipendenze" che consente di risolvere questi problemi ed evitare la necessità di usare un database reale con i test.

Inserimento di dipendenze

Al momento DinnersController è strettamente "accoppiato" alla classe DinnerRepository. "Accoppiamento" si riferisce a una situazione in cui una classe si basa in modo esplicito su un'altra classe per funzionare:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Poiché la classe DinnerRepository richiede l'accesso a un database, la dipendenza strettamente associata della classe DinnersController ha in DinnerRepository finisce per richiedere di avere un database affinché i metodi di azione DinnersController vengano testati.

È possibile aggirare questo problema usando un modello di progettazione denominato "inserimento delle dipendenze", ovvero un approccio in cui le dipendenze (ad esempio le classi di repository che forniscono l'accesso ai dati) non vengono più create in modo implicito all'interno di classi che le usano. Al contrario, le dipendenze possono essere passate in modo esplicito alla classe che le usa usando argomenti del costruttore. Se le dipendenze vengono definite usando le interfacce, è possibile passare implementazioni di dipendenze "false" per gli scenari di unit test. Ciò consente di creare implementazioni di dipendenze specifiche del test che non richiedono effettivamente l'accesso a un database.

Per verificarlo in azione, è possibile implementare l'inserimento delle dipendenze con DinnersController.

Estrazione di un'interfaccia IDinnerRepository

Il primo passaggio consiste nel creare una nuova interfaccia IDinnerRepository che incapsula il contratto del repository che i controller richiedono per recuperare e aggiornare Dinners.

È possibile definire manualmente questo contratto di interfaccia facendo clic con il pulsante destro del mouse sulla cartella \Models e scegliendo il comando di menu Aggiungi nuovo> elemento e creando una nuova interfaccia denominata IDinnerRepository.cs.

In alternativa, è possibile usare gli strumenti di refactoring incorporati in Visual Studio Professional (e versioni successive) per estrarre e creare automaticamente un'interfaccia per noi dalla classe DinnerRepository esistente. Per estrarre questa interfaccia usando Visual Studio, posizionare semplicemente il cursore nell'editor di testo nella classe DinnerRepository, quindi fare clic con il pulsante destro del mouse e scegliere il comando di menu Refactor-Extract> Interface :

Screenshot che mostra l'opzione Extract Interface selezionata nel sottomenu Refactoring.

Verrà avviata la finestra di dialogo "Estrai interfaccia" e verrà richiesto di specificare il nome dell'interfaccia da creare. L'impostazione predefinita è IDinnerRepository e seleziona automaticamente tutti i metodi pubblici nella classe DinnerRepository esistente da aggiungere all'interfaccia:

Screenshot della finestra Risultati test in Visual Studio.

Quando si fa clic sul pulsante "ok", Visual Studio aggiungerà una nuova interfaccia IDinnerRepository all'applicazione:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

E la classe DinnerRepository esistente verrà aggiornata in modo che implementi l'interfaccia:

public class DinnerRepository : IDinnerRepository {
   ...
}

Aggiornamento di DinnersController per supportare l'inserimento del costruttore

Ora aggiorneremo la classe DinnersController per usare la nuova interfaccia.

Attualmente DinnersController è hardcoded in modo che il suo campo "dinnerRepository" sia sempre una classe DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Lo modificheremo in modo che il campo "dinnerRepository" sia di tipo IDinnerRepository anziché DinnerRepository. Verranno quindi aggiunti due costruttori public DinnersController. Uno dei costruttori consente di passare un oggetto IDinnerRepository come argomento. L'altro è un costruttore predefinito che usa l'implementazione di DinnerRepository esistente:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Poiché ASP.NET MVC per impostazione predefinita crea classi controller usando costruttori predefiniti, DinnersController in fase di esecuzione continuerà a usare la classe DinnerRepository per eseguire l'accesso ai dati.

È ora possibile aggiornare gli unit test per passare un'implementazione del repository di cena "fake" usando il costruttore del parametro. Questo repository di cena "falso" non richiederà l'accesso a un database reale e userà invece dati di esempio in memoria.

Creazione della classe FakeDinnerRepository

Verrà ora creata una classe FakeDinnerRepository.

Si inizierà creando una directory "Fakes" all'interno del progetto NerdDinner.Tests e quindi aggiungendo una nuova classe FakeDinnerRepository (fare clic con il pulsante destro del mouse sulla cartella e scegliere Add-New> Class):

Screenshot della voce di menu Aggiungi nuova classe. L'opzione Aggiungi nuovo elemento è evidenziata.

Il codice verrà aggiornato in modo che la classe FakeDinnerRepository implementi l'interfaccia IDinnerRepository. È quindi possibile fare clic con il pulsante destro del mouse su di esso e scegliere il comando di menu di scelta rapida "Implementa interfaccia IDinnerRepository":

Screenshot del comando di menu di scelta rapida Implementa interfaccia I Dinner Repository.

In questo modo Visual Studio aggiunge automaticamente tutti i membri dell'interfaccia IDinnerRepository alla classe FakeDinnerRepository con implementazioni predefinite di "stub out":

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

È quindi possibile aggiornare l'implementazione FakeDinnerRepository per disattivare un insieme List<Dinner> in memoria passato come argomento del costruttore:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

È ora disponibile un'implementazione fittizia di IDinnerRepository che non richiede un database e può invece eseguire un elenco in memoria di oggetti Dinner.

Uso di FakeDinnerRepository con unit test

Torniamo agli unit test dinnersController non riusciti in precedenza perché il database non era disponibile. È possibile aggiornare i metodi di test per usare un FakeDinnerRepository popolato con dati di esempio in memoria Dinner a DinnersController usando il codice seguente:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

E ora, quando si eseguono questi test, entrambi superano:

Screenshot degli unit test, entrambi i test sono stati superati.

Al meglio, richiedono solo una frazione di secondo per l'esecuzione e non richiedono alcuna logica di installazione/pulizia complicata. È ora possibile eseguire unit test di tutto il codice del metodo di azione DinnersController (incluso listato, paging, dettagli, creare, aggiornare ed eliminare) senza dover mai connettersi a un database reale.

Argomento laterale: Framework di inserimento delle dipendenze
L'esecuzione dell'inserimento manuale delle dipendenze (come in precedenza) funziona correttamente, ma diventa più difficile da mantenere man mano che aumenta il numero di dipendenze e componenti in un'applicazione. Esistono diversi framework di inserimento delle dipendenze per .NET che consentono di offrire una maggiore flessibilità di gestione delle dipendenze. Questi framework, detti anche contenitori "Inversion of Control" (IoC), forniscono meccanismi che consentono un livello aggiuntivo di supporto della configurazione per specificare e passare dipendenze agli oggetti in fase di esecuzione (più spesso usando l'inserimento del costruttore). Alcuni dei framework OSS Dependency Injection/IOC più diffusi in .NET includono: AutoFac, Ninject, Spring.NET, StructureMap e Windsor. ASP.NET MVC espone le API di estendibilità che consentono agli sviluppatori di partecipare alla risoluzione e alla creazione di istanze dei controller e che consente l'integrazione pulita dei framework di inserimento delle dipendenze/IoC all'interno di questo processo. L'uso di un framework DI/IOC consente anche di rimuovere il costruttore predefinito da DinnersController, rimuovendo completamente l'accoppiamento tra di esso e DinnerRepository. Non verrà usato un framework di inserimento delle dipendenze/IOC con l'applicazione NerdDinner. Ma è qualcosa che potremmo prendere in considerazione per il futuro se la codebase nerdDinner e le funzionalità sono cresciute.

Creazione di unit test delle azioni di modifica

Verranno ora creati alcuni unit test che verificano la funzionalità Modifica di DinnersController. Si inizierà testando la versione HTTP-GET dell'azione Modifica:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Verrà creato un test che verifica che venga eseguito il rendering di un oggetto View supportato da un oggetto DinnerFormViewModel quando viene richiesta una cena valida:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Quando si esegue il test, tuttavia, si noterà che ha esito negativo perché viene generata un'eccezione di riferimento Null quando il metodo Edit accede alla proprietà User.Identity.Name per eseguire il controllo Dinner.IsHostedBy().

L'oggetto User nella classe di base Controller incapsula i dettagli sull'utente connesso e viene popolato da ASP.NET MVC quando crea il controller in fase di esecuzione. Poiché si sta testando DinnersController all'esterno di un ambiente server Web, l'oggetto User non è impostato (di conseguenza l'eccezione di riferimento Null).

Simulazione della proprietà User.Identity.Name

La simulazione dei framework semplifica il test consentendo di creare in modo dinamico versioni fittizie di oggetti dipendenti che supportano i test. Ad esempio, è possibile usare un framework fittizio nel test dell'azione Modifica per creare dinamicamente un oggetto User che dinnersController può usare per cercare un nome utente simulato. In questo modo si evita che venga generato un riferimento Null quando si esegue il test.

Esistono molti framework di simulazione .NET che possono essere usati con ASP.NET MVC (è possibile visualizzare un elenco di questi framework qui: http://www.mockframeworks.com/).

Dopo il download, aggiungeremo un riferimento al progetto NerdDinner.Tests all'assembly Moq.dll:

Screenshot dell'albero di navigazione Nerd Dinner. Moq è evidenziato.

Si aggiungerà quindi un metodo helper "CreateDinnersControllerAs(username)" alla classe di test che accetta un nome utente come parametro e quindi "simula" la proprietà User.Identity.Name nell'istanza DinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

In precedenza si usa Moq per creare un oggetto Mock che falsi un oggetto ControllerContext ,ovvero ciò che ASP.NET MVC passa alle classi controller per esporre oggetti di runtime come User, Request, Response e Session. Viene chiamato il metodo "SetupGet" in Mock per indicare che la proprietà HttpContext.User.Identity.Name in ControllerContext deve restituire la stringa del nome utente passata al metodo helper.

È possibile simulare qualsiasi numero di proprietà e metodi ControllerContext. Per illustrare questo ho aggiunto anche una chiamata SetupGet() per la proprietà Request.IsAuthenticated (che non è effettivamente necessaria per i test seguenti, ma che illustra come è possibile simulare le proprietà request). Al termine, viene assegnata un'istanza del controllercontext fittizio al metodo helper DinnersController.

È ora possibile scrivere unit test che usano questo metodo helper per testare scenari di modifica che coinvolgono utenti diversi:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

E ora quando si eseguono i test superati:

Screenshot degli unit test che usano il metodo helper. I test sono stati superati.

Test degli scenari UpdateModel()

Sono stati creati test che coprono la versione HTTP-GET dell'azione Modifica. Verranno ora creati alcuni test che verificano la versione HTTP-POST dell'azione Modifica:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

Il nuovo scenario di test interessante da supportare con questo metodo di azione è l'utilizzo del metodo helper UpdateModel() nella classe di base Controller. Questo metodo helper viene usato per associare i valori di post modulo all'istanza dell'oggetto Dinner.

Di seguito sono riportati due test che illustrano come è possibile specificare valori inviati dal modulo per il metodo helper UpdateModel() da usare. Questa operazione verrà eseguita creando e popolando un oggetto FormCollection e quindi assegnandolo alla proprietà "ValueProvider" nel controller.

Il primo test verifica che in un salvataggio riuscito il browser venga reindirizzato all'azione dei dettagli. Il secondo test verifica che quando viene inserito un input non valido, l'azione ripete la visualizzazione di modifica con un messaggio di errore.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Test Wrap-Up

Sono stati illustrati i concetti di base relativi alle classi controller di unit test. È possibile usare queste tecniche per creare facilmente centinaia di test semplici che verificano il comportamento dell'applicazione.

Poiché i test del controller e del modello non richiedono un database reale, sono estremamente veloci e facili da eseguire. Saremo in grado di eseguire centinaia di test automatizzati in pochi secondi e ricevere immediatamente feedback per stabilire se una modifica apportata ha interrotto qualcosa. Ciò consentirà di migliorare, effettuare il refactoring e perfezionare continuamente l'applicazione.

Il test è stato trattato come ultimo argomento di questo capitolo, ma non perché il test è un'operazione da eseguire alla fine di un processo di sviluppo. Al contrario, è consigliabile scrivere test automatizzati il prima possibile nel processo di sviluppo. In questo modo è possibile ottenere feedback immediato durante lo sviluppo, consente di considerare in modo ponderato gli scenari dei casi d'uso dell'applicazione e di progettare l'applicazione con livelli puliti e accoppiamento in mente.

Un capitolo successivo del libro illustra lo sviluppo basato su test (TDD) e come usarlo con ASP.NET MVC. TDD è una procedura di codifica iterativa in cui si scrivono prima i test che il codice risultante soddisfa. Con TDD si inizia ogni funzionalità creando un test che verifica la funzionalità che si sta per implementare. La scrittura dello unit test consente prima di tutto di comprendere chiaramente la funzionalità e il modo in cui dovrebbe funzionare. Solo dopo che il test viene scritto (e si è verificato che ha esito negativo) si implementa quindi la funzionalità effettiva che il test verifica. Poiché si è già trascorso tempo pensando al caso d'uso del funzionamento della funzionalità, si avrà una migliore comprensione dei requisiti e del modo migliore per implementarli. Al termine dell'implementazione, è possibile eseguire nuovamente il test e ottenere commenti e suggerimenti immediati su se la funzionalità funziona correttamente. Verranno illustrati altri TDD nel capitolo 10.

passaggio successivo

Alcuni commenti finali.