Condividi tramite


Iterazione 5 - Creare gli unit test (C#)

di Microsoft

Scaricare il codice

Nella quinta iterazione l'applicazione risulta più semplice da gestire e modificare aggiungendo unit test. Si simulano le classi del modello di dati e si compilano unit test per i controller e la logica di convalida.

Compilazione di un'applicazione MVC ASP.NET Gestione contatti (C#)

In questa serie di esercitazioni viene creata un'intera applicazione Contact Management dall'inizio alla fine. L'applicazione Contact Manager consente di archiviare le informazioni di contatto, ovvero nomi, numeri di telefono e indirizzi di posta elettronica, per un elenco di persone.

L'applicazione viene compilata su più iterazioni. Con ogni iterazione, l'applicazione viene migliorata gradualmente. L'obiettivo di questo approccio a più iterazioni è consentire di comprendere il motivo di ogni modifica.

  • Iterazione n. 1: creare l'applicazione. Nella prima iterazione viene creato il Contact Manager nel modo più semplice possibile. Viene aggiunto il supporto per le operazioni di base del database: Create, Read, Update e Delete (CRUD).

  • Iterazione n. 2: rendere l'applicazione più bella. In questa iterazione viene migliorata l'aspetto dell'applicazione modificando la pagina master della visualizzazione MVC predefinita ASP.NET e il foglio di stile css.

  • Iterazione n. 3: aggiungere la convalida dei moduli. Nella terza iterazione viene aggiunta la convalida dei moduli di base. Microsoft impedisce agli utenti di inviare un modulo senza completare i campi modulo obbligatori. Vengono convalidati anche gli indirizzi di posta elettronica e i numeri di telefono.

  • Iterazione n. 4: rendere l'applicazione ad accoppiamento libero. In questa quarta iterazione si sfruttano diversi modelli di progettazione software per semplificare la gestione e la modifica dell'applicazione Contact Manager. Ad esempio, si esegue il refactoring dell'applicazione per usare il modello repository e il modello di inserimento delle dipendenze.

  • Iterazione n. 5: creare unit test. Nella quinta iterazione l'applicazione risulta più semplice da gestire e modificare aggiungendo unit test. Si simulano le classi del modello di dati e si compilano unit test per i controller e la logica di convalida.

  • Iterazione n. 6: usare lo sviluppo basato su test. In questa sesta iterazione si aggiungono nuove funzionalità all'applicazione scrivendo prima unit test e scrivendo codice per gli unit test. In questa iterazione si aggiungono gruppi di contatti.

  • Iterazione n. 7: aggiungere la funzionalità Ajax. Nella settima iterazione viene migliorata la velocità di risposta e le prestazioni dell'applicazione aggiungendo il supporto per Ajax.

Iterazione

Nell'iterazione precedente dell'applicazione Contact Manager è stato refactoring dell'applicazione in modo che sia più ad accoppiamento libero. L'applicazione è stata separata in livelli di controller, servizio e repository distinti. Ogni livello interagisce con il livello sottostante tramite interfacce.

È stato refactoring dell'applicazione per semplificare la gestione e la modifica dell'applicazione. Ad esempio, se è necessario usare una nuova tecnologia di accesso ai dati, è possibile modificare semplicemente il livello del repository senza toccare il controller o il livello di servizio. Grazie all'accoppiamento libero di Contact Manager, l'applicazione è stata resa più resiliente alla modifica.

Ma cosa accade quando è necessario aggiungere una nuova funzionalità all'applicazione Contact Manager? In alternativa, cosa accade quando si corregge un bug? Un triste, ma ben dimostrato, la verità di scrivere codice è che ogni volta che si tocca il codice si crea il rischio di introdurre nuovi bug.

Ad esempio, un giorno, il responsabile potrebbe chiedere di aggiungere una nuova funzionalità a Contact Manager. Vuole aggiungere il supporto per i gruppi di contatti. Vuole consentire agli utenti di organizzare i propri contatti in gruppi come Amici, Affari e così via.

Per implementare questa nuova funzionalità, è necessario modificare tutti e tre i livelli dell'applicazione Contact Manager. Sarà necessario aggiungere nuove funzionalità ai controller, al livello di servizio e al repository. Non appena si inizia a modificare il codice, si rischia di interrompere le funzionalità che funzionano in precedenza.

Il refactoring dell'applicazione in livelli separati, come è stato fatto nell'iterazione precedente, è una buona cosa. È stata una buona cosa perché consente di apportare modifiche a interi livelli senza toccare il resto dell'applicazione. Tuttavia, se si vuole semplificare la manutenzione e la modifica del codice all'interno di un livello, è necessario creare unit test per il codice.

Si usa uno unit test per testare una singola unità di codice. Queste unità di codice sono inferiori agli interi livelli dell'applicazione. In genere, si usa uno unit test per verificare se un particolare metodo nel codice si comporta nel modo previsto. Ad esempio, si creerebbe uno unit test per il metodo CreateContact() esposto dalla classe ContactManagerService.

Gli unit test per un'applicazione funzionano esattamente come una rete di sicurezza. Ogni volta che si modifica il codice in un'applicazione, è possibile eseguire un set di unit test per verificare se la modifica interrompe la funzionalità esistente. Gli unit test rendono il codice sicuro da modificare. Gli unit test rendono tutto il codice nell'applicazione più resiliente alle modifiche.

In questa iterazione si aggiungono unit test all'applicazione Contact Manager. In questo modo, nell'iterazione successiva, è possibile aggiungere gruppi di contatti all'applicazione senza doversi preoccupare dell'interruzione delle funzionalità esistenti.

Nota

Sono disponibili diversi framework di unit test, tra cui NUnit, xUnit.net e MbUnit. In questa esercitazione viene usato il framework di unit test incluso in Visual Studio. Tuttavia, è possibile usare facilmente uno di questi framework alternativi.

Che cosa viene testato

Nel mondo perfetto, tutto il codice sarebbe coperto dagli unit test. Nel mondo perfetto, avresti la rete di sicurezza perfetta. Sarebbe possibile modificare qualsiasi riga di codice nell'applicazione e sapere immediatamente, eseguendo gli unit test, se la modifica ha interrotto la funzionalità esistente.

Tuttavia, non viviamo in un mondo perfetto. In pratica, quando si scrivono unit test, ci si concentra sulla scrittura di test per la logica di business, ad esempio la logica di convalida. In particolare, non si scrivono unit test per la logica di accesso ai dati o la logica di visualizzazione.

Per essere utile, gli unit test devono essere eseguiti molto rapidamente. È possibile accumulare facilmente centinaia (o anche migliaia) di unit test per un'applicazione. Se l'esecuzione degli unit test richiede molto tempo, è consigliabile evitare di eseguirle. In altre parole, gli unit test a esecuzione prolungata sono inutili per scopi di codifica quotidiani.

Per questo motivo, in genere non si scrivono unit test per il codice che interagisce con un database. L'esecuzione di centinaia di unit test su un database attivo sarebbe troppo lenta. È invece possibile simulare il database e scrivere codice che interagisce con il database fittizio (viene illustrato come simulare un database di seguito).

Per un motivo simile, in genere non si scrivono unit test per le visualizzazioni. Per testare una visualizzazione, è necessario attivare un server Web. Poiché la rotazione di un server Web è un processo relativamente lento, non è consigliabile creare unit test per le visualizzazioni.

Se la visualizzazione contiene logica complessa, è consigliabile spostare la logica nei metodi helper. È possibile scrivere unit test per i metodi helper eseguiti senza eseguire un server Web.

Nota

Durante la scrittura di test per la logica di accesso ai dati o la logica di visualizzazione non è consigliabile scrivere unit test, questi test possono essere molto utili durante la compilazione di test funzionali o di integrazione.

Nota

ASP.NET MVC è il motore di visualizzazione Web Forms. Anche se il motore di visualizzazione Web Forms dipende da un server Web, altri motori di visualizzazione potrebbero non essere.

Uso di un framework di oggetti fittizi

Quando si compilano unit test, è quasi sempre necessario sfruttare un framework di oggetti fittizi. Un framework di oggetti fittizi consente di creare mock e stub per le classi nell'applicazione.

Ad esempio, è possibile usare un framework di oggetti fittizi per generare una versione fittizia della classe del repository. In questo modo, è possibile usare la classe del repository fittizio anziché la classe del repository reale negli unit test. L'uso del repository fittizio consente di evitare di eseguire codice di database durante l'esecuzione di uno unit test.

Visual Studio non include un framework di oggetti fittizi. Esistono tuttavia diversi framework commerciali e open source Mock Object framework disponibili per .NET Framework:

  1. Moq: questo framework è disponibile nella licenza BSD open source. È possibile scaricare Moq da https://code.google.com/p/moq/.
  2. Rhino Mocks: questo framework è disponibile nella licenza BSD open source. È possibile scaricare Rhino Mocks da http://ayende.com/projects/rhino-mocks.aspx.
  3. Typemock Isolator : si tratta di un framework commerciale. È possibile scaricare una versione di valutazione da http://www.typemock.com/.

In questa esercitazione ho deciso di usare Moq. Tuttavia, è possibile usare facilmente Rhino Mocks o Typemock Isolator per creare gli oggetti Mock per l'applicazione Contact Manager.

Prima di poter usare Moq, è necessario completare la procedura seguente:

  1. .
  2. Prima di decomprimere il download, assicurarsi di fare clic con il pulsante destro del mouse sul file e fare clic sul pulsante Con etichetta Sblocca (vedere La figura 1).
  3. Decomprimere il download.
  4. Aggiungere un riferimento all'assembly Moq facendo clic con il pulsante destro del mouse sulla cartella Riferimenti nel progetto ContactManager.Test e selezionando Aggiungi riferimento. Nella scheda Sfoglia passare alla cartella in cui è stato deselezionato Moq e selezionare l'assembly Moq.dll. Fare clic sul pulsante OK .
  5. Al termine di questi passaggi, la cartella Riferimenti dovrebbe essere simile alla figura 2.

Sblocco di Moq

Figura 01: Sblocco di Moq(Fare clic per visualizzare l'immagine full-size)

Riferimenti dopo l'aggiunta di Moq

Figura 02: Riferimenti dopo l'aggiunta di Moq(Fare clic per visualizzare l'immagine full-size)

Creazione di unit test per il livello di servizio

Iniziamo creando un set di unit test per il livello di servizio dell'applicazione Contact Manager. Questi test verranno usati per verificare la logica di convalida.

Creare una nuova cartella denominata Models nel progetto ContactManager.Tests. Fare quindi clic con il pulsante destro del mouse sulla cartella Modelli e scegliere Aggiungi, Nuovo test. Viene visualizzata la finestra di dialogo Aggiungi nuovo test illustrata nella figura 3. Selezionare il modello unit test e assegnare un nome al nuovo test ContactManagerServiceTest.cs. Fare clic sul pulsante OK per aggiungere il nuovo test al progetto di test.

Nota

In generale, si vuole che la struttura della cartella del progetto di test corrisponda alla struttura di cartelle del progetto MVC ASP.NET. Ad esempio, si inserisce test controller in una cartella Controller, test del modello in una cartella Models e così via.

Models\ContactManagerServiceTest.cs

Figura 03: Models\ContactManagerServiceTest.cs(Fare clic per visualizzare l'immagine full-size)

Inizialmente, si vuole testare il metodo CreateContact() esposto dalla classe ContactManagerService. Verranno creati i cinque test seguenti:

  • CreateContact() : verifica che CreateContact() restituisce il valore true quando un contatto valido viene passato al metodo.
  • CreateContactRequiredFirstName() - Verifica che venga aggiunto un messaggio di errore allo stato del modello quando un contatto con un nome mancante viene passato al metodo CreateContact().
  • CreateContactRequiredLastName() - Verifica che venga aggiunto un messaggio di errore allo stato del modello quando un contatto con un cognome mancante viene passato al metodo CreateContact().
  • CreateContactInvalidPhone() - Verifica che venga aggiunto un messaggio di errore allo stato del modello quando un contatto con un numero di telefono non valido viene passato al metodo CreateContact().
  • CreateContactInvalidEmail() - Verifica che venga aggiunto un messaggio di errore allo stato del modello quando un contatto con un indirizzo di posta elettronica non valido viene passato al metodo CreateContact().

Il primo test verifica che un contatto valido non generi un errore di convalida. I test rimanenti controllano ognuna delle regole di convalida.

Il codice per questi test è contenuto nell'elenco 1.

Elenco 1 - Modelli\ContactManagerServiceTest.cs

using System.Web.Mvc;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Models
{
    [TestClass]
    public class ContactManagerServiceTest
    {
        private Mock<IContactManagerRepository> _mockRepository;
        private ModelStateDictionary _modelState;
        private IContactManagerService _service;

        [TestInitialize]
        public void Initialize()
        {
            _mockRepository = new Mock<IContactManagerRepository>();
            _modelState = new ModelStateDictionary();
            _service = new ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object);
        }

        [TestMethod]
        public void CreateContact()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);
        
            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void CreateContactRequiredFirstName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, string.Empty, "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["FirstName"].Errors[0];
            Assert.AreEqual("First name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactRequiredLastName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", string.Empty, "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["LastName"].Errors[0];
            Assert.AreEqual("Last name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidPhone()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Phone"].Errors[0];
            Assert.AreEqual("Invalid phone number.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidEmail()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Email"].Errors[0];
            Assert.AreEqual("Invalid email address.", error.ErrorMessage);
        }
    }
}

Poiché si usa la classe Contact nell'elenco 1, è necessario aggiungere un riferimento al progetto di test di Microsoft Entity Framework. Aggiungere un riferimento all'assembly System.Data.Entity.

L'elenco 1 contiene un metodo denominato Initialize() decorato con l'attributo [TestInitialize]. Questo metodo viene chiamato automaticamente prima che ogni unit test venga eseguito (viene chiamato 5 volte prima di ognuno degli unit test). Il metodo Initialize() crea un repository fittizio con la riga di codice seguente:

_mockRepository = new Mock<IContactManagerRepository>();

Questa riga di codice usa il framework Moq per generare un repository fittizio dall'interfaccia IContactManagerRepository. Il repository fittizio viene usato anziché l'effettivo EntityContactManagerRepository per evitare di accedere al database quando viene eseguito ogni unit test. Il repository fittizio implementa i metodi dell'interfaccia IContactManagerRepository, ma i metodi non fanno effettivamente nulla.

Nota

Quando si usa il framework Moq, esiste una distinzione tra _mockRepository e _mockRepository.Object. Il precedente fa riferimento alla classe Mock<IContactManagerRepository> che contiene metodi per specificare il comportamento del repository fittizio. Quest'ultimo fa riferimento al repository fittizio effettivo che implementa l'interfaccia IContactManagerRepository.

Il repository fittizio viene usato nel metodo Initialize() durante la creazione di un'istanza della classe ContactManagerService. Tutti i singoli unit test usano questa istanza della classe ContactManagerService.

L'elenco 1 contiene cinque metodi che corrispondono a ognuno degli unit test. Ognuno di questi metodi viene decorato con l'attributo [TestMethod]. Quando si eseguono gli unit test, viene chiamato qualsiasi metodo con questo attributo. In altre parole, qualsiasi metodo decorato con l'attributo [TestMethod] è un unit test.

Il primo unit test, denominato CreateContact(), verifica che la chiamata a CreateContact() restituisce il valore true quando viene passata un'istanza valida della classe Contact al metodo . Il test crea un'istanza della classe Contact, chiama il metodo CreateContact() e verifica che CreateContact() restituisca il valore true.

I test rimanenti verificano che quando il metodo CreateContact() viene chiamato con un contatto non valido, il metodo restituisce false e il messaggio di errore di convalida previsto viene aggiunto allo stato del modello. Ad esempio, il test CreateContactRequiredFirstName() crea un'istanza della classe Contact con una stringa vuota per la proprietà FirstName. Successivamente, il metodo CreateContact() viene chiamato con il contatto non valido. Infine, il test verifica che CreateContact() restituisce false e lo stato del modello contiene il messaggio di errore di convalida previsto "Nome è obbligatorio".

È possibile eseguire gli unit test nell'elenco 1 selezionando l'opzione di menu Test, Esegui, Tutti i test nella soluzione (CTRL+R, A). I risultati dei test vengono visualizzati nella finestra Risultati test (vedere la figura 4).

Risultati dei test

Figura 04: Risultati dei test (Fare clic per visualizzare l'immagine full-size)

Creazione di unit test per i controller

ASP. L'applicazione NETMVC controlla il flusso di interazione utente. Quando si esegue il test di un controller, si vuole verificare se il controller restituisce il risultato dell'azione corretto e visualizzare i dati. È anche possibile verificare se un controller interagisce con le classi di modello nel modo previsto.

L'elenco 2, ad esempio, contiene due unit test per il metodo Contact controller Create(). Il primo unit test verifica che quando un contatto valido viene passato al metodo Create() e quindi il metodo Create() reindirizza all'azione Index. In altre parole, quando è stato passato un contatto valido, il metodo Create() deve restituire un reindirizzamentoToRouteResult che rappresenta l'azione Index.

Non si vuole testare il livello di servizio ContactManager quando si esegue il test del livello controller. Di conseguenza, viene simulato il livello di servizio con il codice seguente nel metodo Initialize:

_service = new Mock();

Nell'unit test createValidContact() viene simulato il comportamento di chiamare il metodo CreateContact() del livello di servizio con la riga di codice seguente:

_service.Expect(s => s.CreateContact(contact)).Returns(true);

Questa riga di codice fa sì che il servizio ContactManager fittizio restituisca il valore true quando viene chiamato il relativo metodo CreateContact(). Simulando il livello di servizio, è possibile testare il comportamento del controller senza dover eseguire codice nel livello di servizio.

Il secondo unit test verifica che l'azione Create() restituisce la visualizzazione Create quando un contatto non valido viene passato al metodo. Il metodo CreateContact() del livello di servizio restituisce il valore false con la riga di codice seguente:

_service.Expect(s => s.CreateContact(contact)).Returns(false);

Se il metodo Create() si comporta come previsto, dovrebbe restituire la visualizzazione Create quando il livello del servizio restituisce il valore false. In questo modo, il controller può visualizzare i messaggi di errore di convalida nella visualizzazione Crea e l'utente ha la possibilità di correggere le proprietà contatto non valide.

Se si prevede di compilare unit test per i controller, è necessario restituire nomi di visualizzazione espliciti dalle azioni del controller. Ad esempio, non restituire una visualizzazione simile al seguente:

return View();

Restituisce invece la visualizzazione simile alla seguente:

view return("Create");

Se non si è espliciti quando si restituisce una visualizzazione, la proprietà ViewResult.ViewName restituisce una stringa vuota.

Elenco 2 - Controller\ContactControllerTest.cs

using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Controllers
{
    [TestClass]
    public class ContactControllerTest
    {
        private Mock<IContactManagerService> _service;

        [TestInitialize]
        public void Initialize()
        {
            _service = new Mock<IContactManagerService>();
        }

        [TestMethod]
        public void CreateValidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(true);
            var controller = new ContactController(_service.Object);
        
            // Act
            var result = (RedirectToRouteResult)controller.Create(contact);

            // Assert
            Assert.AreEqual("Index", result.RouteValues["action"]);
        }

        [TestMethod]
        public void CreateInvalidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(false);
            var controller = new ContactController(_service.Object);

            // Act
            var result = (ViewResult)controller.Create(contact);

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

    }
}

Riepilogo

In questa iterazione sono stati creati unit test per l'applicazione Contact Manager. È possibile eseguire questi unit test in qualsiasi momento per verificare che l'applicazione si comporta ancora nel modo previsto. Gli unit test fungono da rete di sicurezza per l'applicazione che consente di modificare in modo sicuro l'applicazione in futuro.

Sono stati creati due set di unit test. Prima di tutto, è stata testata la logica di convalida creando unit test per il livello di servizio. Successivamente, è stata testata la logica di controllo del flusso creando unit test per il livello controller. Durante il test del livello di servizio, sono stati isolati i test per il livello di servizio dal livello del repository simulando il livello del repository. Durante il test del livello controller, i test sono stati isolati per il livello controller simulando il livello di servizio.

Nell'iterazione successiva viene modificata l'applicazione Contact Manager in modo che supporti i gruppi di contatti. Questa nuova funzionalità verrà aggiunta all'applicazione usando un processo di progettazione software denominato sviluppo basato su test.