Aktivieren von automatisierten Komponententests

von Microsoft

PDF herunterladen

Dies ist Schritt 12 eines kostenlosen Lernprogramms zur Anwendung "NerdDinner" , das durchläuft, wie sie eine kleine, aber vollständige Webanwendung mit ASP.NET MVC 1 erstellen.

In Schritt 12 wird gezeigt, wie Sie eine Reihe automatisierter Komponententests entwickeln, die unsere NerdDinner-Funktionalität überprüfen und die uns das Vertrauen geben, änderungen und Verbesserungen an der Anwendung in Zukunft vorzunehmen.

Wenn Sie ASP.NET MVC 3 verwenden, empfehlen wir Ihnen, den Lernprogrammen " Erste Schritte mit MVC 3 " oder "MVC Music Store " zu folgen.

NerdDinner Schritt 12: Komponententests

Lassen Sie uns eine Reihe automatisierter Komponententests entwickeln, die unsere NerdDinner-Funktionalität überprüfen und uns das Vertrauen geben, änderungen und Verbesserungen an der Anwendung in Zukunft vorzunehmen.

Warum Komponententest?

Auf der Fahrt in die Arbeit einen Morgen haben Sie einen plötzlichen Inspirationsblitz über eine Anwendung, an der Sie arbeiten. Sie erkennen, dass es eine Änderung gibt, die Sie implementieren können, um die Anwendung erheblich zu verbessern. Möglicherweise handelt es sich um eine Umgestaltung, die den Code bereinigt, ein neues Feature hinzufügt oder einen Fehler behebt.

Die Frage, die Sie konfrontiert, wenn Sie auf Ihrem Computer ankommen, ist – "wie sicher ist es, diese Verbesserung zu machen?" Was geschieht, wenn die Änderung Nebenwirkungen hat oder etwas bricht? Die Änderung kann einfach sein und dauert nur ein paar Minuten, aber was ist, wenn es Stunden dauert, um alle Anwendungsszenarien manuell zu testen? Was geschieht, wenn Sie vergessen, ein Szenario abzudecken und eine fehlerhafte Anwendung in die Produktion geht? Lohnt sich diese Verbesserung wirklich alle Anstrengungen?

Automatisierte Komponententests können ein Sicherheitsnetz bereitstellen, das es Ihnen ermöglicht, Ihre Anwendungen kontinuierlich zu verbessern und keine Angst vor dem Code zu haben, an dem Sie arbeiten. Mit automatisierten Tests, die die Funktionalität schnell überprüfen, können Sie mit Zuversicht programmieren – und Verbesserungen vornehmen, die Sie sich sonst nicht getraut hätten vorzunehmen. Sie helfen auch bei der Erstellung von Lösungen, die wartungsfähiger sind und eine längere Lebensdauer haben – was zu einer viel höheren Rendite für Investitionen führt.

Das ASP.NET MVC Framework macht es einfach und natürlich, Unit-Tests für Anwendungsfunktionen durchzuführen. Außerdem wird ein TDD-Workflow (Test Driven Development) ermöglicht, der eine testbasierte Entwicklung erlaubt.

NerdDinner.Tests Project

Als wir unsere NerdDinner-Anwendung am Anfang dieses Lernprogramms erstellt haben, wurden wir mit einem Dialogfeld gefragt, ob wir ein Komponententestprojekt erstellen wollten, um mit dem Anwendungsprojekt zu arbeiten:

Screenshot des Dialogfelds

Wir haben das Optionsfeld "Ja, Erstellen eines Komponententestprojekts" ausgewählt – was dazu führte, dass unserer Lösung ein Projekt "NerdDinner.Tests" hinzugefügt wurde:

Screenshot der Navigationsstruktur des Projektmappen-Explorers. Nerd Dinner dot Tests ist ausgewählt.

Das Projekt NerdDinner.Tests verweist auf die NerdDinner-Anwendungsprojektassembly und ermöglicht es uns, dem Projekt automatisierte Tests hinzuzufügen, die die Anwendungsfunktionalität überprüfen.

Erstellen von Komponententests für unsere Dinner Model Class

Fügen wir unserem Projekt "NerdDinner.Tests" einige Tests hinzu, die die von uns beim Erstellen unserer Modellebene erstellte Dinner-Klasse überprüfen.

Zunächst erstellen wir einen neuen Ordner in unserem Testprojekt namens "Modelle", in dem wir unsere modellbezogenen Tests platzieren. Klicken Sie dann mit der rechten Maustaste auf den Ordner, und wählen Sie den Menübefehl " Add-New> Test " aus. Dadurch wird das Dialogfeld "Neuen Test hinzufügen" angezeigt.

Wir wählen das Erstellen eines "Unit-Tests" aus und nennen ihn "DinnerTest.cs".

Screenshot des Dialogfelds

Wenn wir auf die Schaltfläche "OK" klicken, fügt Visual Studio dem Projekt eine DinnerTest.cs Datei hinzu (und öffnet sie):

Screenshot der Datei

Die Standardmäßige Visual Studio-Komponententestvorlage enthält eine Reihe von Kesselplattencode darin, dass ich ein wenig unübersichtlich finde. Bereinigen wir es, damit es nur den folgenden Code umfasst:

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 {

    }
}

Das [TestClass]-Attribut der obigen DinnerTest-Klasse identifiziert es als Klasse, die Tests sowie optionalen Testinitialisierungs- und Teardowncode enthält. Wir können Tests darin definieren, indem wir öffentliche Methoden hinzufügen, die über ein [TestMethod]-Attribut verfügen.

Im Folgenden finden Sie die ersten von zwei Tests, die wir hinzufügen werden, um unsere Dinner-Klasse zu üben. Der erste Test überprüft, ob unser Dinner ungültig ist, wenn ein neues Dinner erstellt wird, ohne dass alle Eigenschaften richtig festgelegt werden. Der zweite Test überprüft, ob unser Abendessen gültig ist, wenn ein Dinner alle Eigenschaften mit gültigen Werten festgelegt hat:

[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);
    }
}

Sie werden oben feststellen, dass unsere Testnamen sehr explizit (und etwas ausführlich) sind. Wir tun dies, da wir möglicherweise Hunderte oder Tausende kleiner Tests erstellen, und wir möchten es einfach machen, die Absicht und das Verhalten der einzelnen Tests schnell zu bestimmen (insbesondere wenn wir eine Liste der Fehler in einem Testläufer durchsehen). Die Testnamen sollten nach der Funktionalität benannt werden, die sie testen. Oben verwenden wir eine Namenskonvention im Format "Noun_Should_Verb".

Wir strukturieren die Tests mithilfe des "AAA"-Testmusters – das für "Arrange, Act, Assert" steht:

  • Anordnen: Einrichten der getesteten Einheit
  • Durchführen: Ausführen der zu testenden Einheit und Erfassen der Ergebnisse
  • Assert: Verhalten überprüfen

Wenn wir Tests schreiben, möchten wir vermeiden, dass die einzelnen Tests zu viel tun. Stattdessen sollte jeder Test nur ein einzelnes Konzept überprüfen (wodurch es viel einfacher wird, die Ursache von Fehlern zu bestimmen). Eine gute Richtlinie besteht darin, nur eine einzige Assert-Anweisung für jeden Test zu verwenden. Wenn Sie mehr als eine Assert-Anweisung in einer Testmethode haben, stellen Sie sicher, dass sie alle zum Testen desselben Konzepts verwendet werden. Machen Sie im Zweifelsfall einen anderen Test.

Tests werden ausgeführt

Visual Studio 2008 Professional (und höhere Editionen) enthält einen integrierten Testläufer, der zum Ausführen von Visual Studio-Komponententestprojekten innerhalb der IDE verwendet werden kann. Wir können den Befehl "Test->Run->All Tests in Solution" (oder STRG R, A eingeben) im Menü 'Lösung' auswählen, um alle Unit-Tests auszuführen. Alternativ können wir den Cursor innerhalb einer bestimmten Testklasse oder Testmethode positionieren und den Befehl Test->Run->Tests in Current Context (oder die Tastenkombination Strg R, T) verwenden, um eine Teilmenge der Unittests auszuführen.

Positionieren wir unseren Cursor innerhalb der DinnerTest-Klasse und geben "STRG R, T" ein, um die beiden gerade definierten Tests auszuführen. Wenn wir dies tun, wird ein Fenster "Testergebnisse" in Visual Studio angezeigt, und die Ergebnisse unserer Testausführung werden darin aufgeführt:

Screenshot des Fensters

Hinweis: Das Fenster "VS-Testergebnisse" zeigt standardmäßig nicht die Spalte "Klassenname" an. Sie können dies hinzufügen, indem Sie im Fenster "Testergebnisse" mit der rechten Maustaste klicken und den Menübefehl "Spalten hinzufügen/entfernen" verwenden.

Unsere beiden Tests dauerten nur einen Bruchteil einer Sekunde, um ausgeführt zu werden – und wie Sie sehen können, haben beide bestanden. Wir können sie jetzt weiter ausführen und erweitern, indem wir zusätzliche Tests erstellen, die bestimmte Regelüberprüfungen überprüfen, sowie die beiden Hilfsmethoden " IsUserHost() und IsUserRegistered() abdecken, die wir der Dinner-Klasse hinzugefügt haben. All diese Tests für den Dinner-Kurs werden es viel einfacher und sicherer machen, neue Geschäftsregeln und Validierungen in Zukunft hinzuzufügen. Wir können unsere neue Regellogik zu Dinner hinzufügen und dann innerhalb von Sekunden überprüfen, ob sie keine unserer vorherigen Logikfunktionen beschädigt hat.

Beachten Sie, dass die Verwendung eines beschreibenden Testnamens das schnelle Verständnis der Überprüfungen der einzelnen Tests erleichtert. Ich empfehle, den Menübefehls „Extras->Optionen“ zu verwenden, den Konfigurationsbildschirm „Testtools->Testausführung“ zu öffnen und das Kontrollkästchen „Doppelklicken auf ein fehlgeschlagenes oder nicht schlüssiges Komponententestergebnis zeigt den Fehlerpunkt im Test an“ zu aktivieren. Dadurch können Sie im Testergebnisfenster auf einen Fehler doppelklicken und sofort zum Bestätigungsfehler springen.

Erstellen von DinnersController-Komponententests

Wir erstellen nun einige Komponententests, die unsere DinnersController-Funktionalität überprüfen. Wir beginnen, indem wir mit der rechten Maustaste auf den Ordner "Controllers" in unserem Testprojekt klicken und dann den Befehl "Hinzufügen > Neuer Test" auswählen. Wir erstellen einen "Komponententest" und nennen ihn "DinnersControllerTest.cs".

Wir erstellen zwei Testmethoden, mit denen die Action-Methode "Details()" auf dem DinnersController überprüft wird. Die erste überprüft, ob eine Ansicht zurückgegeben wird, wenn ein vorhandenes Dinner angefordert wird. Die zweite überprüft, ob eine "NotFound"-Ansicht zurückgegeben wird, wenn ein nicht vorhandenes Abendessen angefordert wird:

[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);
    } 
}

Der obige Code kompiliert sauber. Wenn wir die Tests ausführen, schlagen beide jedoch fehl:

Screenshot des Codes. Beide Tests sind fehlgeschlagen.

Wenn wir die Fehlermeldungen betrachten, sehen wir, dass der Grund für die Fehlgeschlagenen Tests war, weil unsere DinnersRepository-Klasse keine Verbindung mit einer Datenbank herstellen konnte. Unsere NerdDinner-Anwendung verwendet eine Verbindungszeichenfolge zu einer lokalen SQL Server Express-Datei, die sich im Verzeichnis \App_Data des NerdDinner-Anwendungsprojekts befindet. Da unser NerdDinner.Tests-Projekt kompiliert und in einem anderen Verzeichnis ausgeführt wird, ist der relative Pfadspeicherort unserer Verbindungszeichenfolge falsch.

Wir konnten dies beheben, indem wir die SQL Express-Datenbankdatei in unser Testprojekt kopieren und dann im App.config unseres Testprojekts eine entsprechende Verbindungszeichenfolge hinzufügen. Dies würde die oben genannten Tests freischalten und starten.

Komponententestcode, der eine echte Datenbank verwendet, bringt jedoch eine Reihe von Herausforderungen mit sich. Dies gilt insbesondere in folgenden Fällen:

  • Die Ausführungszeit von Komponententests wird erheblich verlangsamt. Je länger die Ausführung von Tests dauert, desto geringer ist die Wahrscheinlichkeit, dass Sie sie häufig ausführen. Im Idealfall möchten Sie, dass die Unittests in Sekunden ausgeführt werden können – und es sollte etwas sein, das Sie genauso selbstverständlich tun wie das Kompilieren des Projekts.
  • Sie erschwert die Setup- und Bereinigungslogik innerhalb von Tests. Sie möchten, dass jeder Komponententest isoliert und unabhängig von anderen (ohne Nebenwirkungen oder Abhängigkeiten) ist. Bei der Arbeit mit einer echten Datenbank müssen Sie auf den Zustand achten und ihn zwischen den Tests zurücksetzen.

Sehen wir uns ein Entwurfsmuster namens "Abhängigkeitsinjektion" an, das uns dabei helfen kann, diese Probleme zu umgehen und die Notwendigkeit zu vermeiden, eine echte Datenbank mit unseren Tests zu verwenden.

Abhängigkeitsinjektion

Derzeit ist DinnersController eng "gekoppelt" mit der DinnerRepository Klasse. "Kopplung" bezieht sich auf eine Situation, in der eine Klasse explizit auf eine andere Klasse angewiesen ist, um zu arbeiten:

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);
    }

Da die DinnerRepository-Klasse Zugriff auf eine Datenbank erfordert, erfordert die eng gekoppelte Abhängigkeit der DinnersController-Klasse am DinnerRepository, dass wir eine Datenbank haben müssen, damit die DinnersController-Aktionsmethoden getestet werden können.

Wir können dies umgehen, indem wir ein Entwurfsmuster namens "Abhängigkeitsinjektion" verwenden – ein Ansatz, bei dem Abhängigkeiten (z. B. Repositoryklassen, die Datenzugriff bereitstellen) nicht mehr implizit innerhalb von Klassen erstellt werden, die sie verwenden. Stattdessen können Abhängigkeiten explizit an die Klasse übergeben werden, die sie mithilfe von Konstruktorargumenten verwendet. Wenn die Abhängigkeiten mithilfe von Schnittstellen definiert werden, haben wir die Flexibilität, "gefälschte" Abhängigkeitsimplementierungen für Komponententestszenarien zu übergeben. Auf diese Weise können wir testspezifische Abhängigkeitsimplementierungen erstellen, die keinen Zugriff auf eine Datenbank erfordern.

Um dies in Aktion zu sehen, implementieren wir die Abhängigkeitsinjektion mit unserem DinnersController.

Extrahieren einer IDinnerRepository-Schnittstelle

Unser erster Schritt wird sein, eine neue IDinnerRepository-Schnittstelle zu erstellen, die den Repository-Vertrag kapselt, den unsere Controller zum Abrufen und Aktualisieren von Dinners benötigen.

Wir können diesen Schnittstellenvertrag manuell definieren, indem wir mit der rechten Maustaste auf den Ordner \Models klicken und dann den Menübefehl "Neues Element hinzufügen>" auswählen und eine neue Schnittstelle namens IDinnerRepository.cs erstellen.

Alternativ können wir die in Visual Studio Professional (und höheren Editionen) integrierten Umgestaltungstools verwenden, um automatisch eine Schnittstelle für uns aus unserer vorhandenen DinnerRepository-Klasse zu extrahieren und zu erstellen. Um diese Schnittstelle mit VS zu extrahieren, positionieren Sie einfach den Cursor im Text-Editor in der DinnerRepository-Klasse, und klicken Sie dann mit der rechten Maustaste, und wählen Sie den Menübefehl "Schnittstelle umgestalten>" aus:

Screenshot, der das im Untermenü

Dadurch wird das Dialogfeld "Schnittstelle extrahieren" gestartet, und wir werden aufgefordert, den Namen der zu erstellenden Schnittstelle anzugeben. Standardmäßig wird auf IDinnerRepository zurückgegriffen, und alle öffentlichen Methoden der vorhandenen DinnerRepository-Klasse werden automatisch ausgewählt, um sie der Schnittstelle hinzuzufügen.

Screenshot des Fensters

Wenn wir auf die Schaltfläche "OK" klicken, fügt Visual Studio unserer Anwendung eine neue IDinnerRepository-Schnittstelle hinzu:

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();
}

Und unsere vorhandene DinnerRepository-Klasse wird aktualisiert, damit sie die Schnittstelle implementiert:

public class DinnerRepository : IDinnerRepository {
   ...
}

Aktualisieren von DinnersController zur Unterstützung der Konstruktorinjektion

Wir aktualisieren nun die DinnersController-Klasse, um die neue Schnittstelle zu verwenden.

Derzeit ist DinnersController hartcodiert, sodass das Feld "dinnerRepository" immer eine DinnerRepository-Klasse ist:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Wir ändern es so, dass das Feld "dinnerRepository" vom Typ "IDinnerRepository" anstelle von DinnerRepository ist. Anschließend fügen wir zwei öffentliche DinnersController Konstruktoren hinzu. Einer der Konstruktoren ermöglicht die Übergabe eines IDinnerRepository als Argument. Die andere ist ein Standardkonstruktor, der unsere vorhandene DinnerRepository-Implementierung verwendet:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

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

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

Da ASP.NET MVC standardmäßig Controllerklassen mit Standardkonstruktoren erstellt, verwendet unser DinnersController zur Laufzeit weiterhin die DinnerRepository-Klasse zum Ausführen des Datenzugriffs.

Wir können nun jedoch unsere Unit-Tests aktualisieren, um eine "Mock"-Dinner-Repository-Implementierung mit dem Parameterkonstruktor zu übergeben. Dieses "gefälschte" Dinner-Repository erfordert keinen Zugriff auf eine echte Datenbank und verwendet stattdessen In-Memory-Beispieldaten.

Erstellen der FakeDinnerRepository-Klasse

Erstellen wir nun eine FakeDinnerRepository-Klasse.

Wir beginnen mit dem Erstellen eines "Fakes"-Verzeichnisses in unserem Projekt NerdDinner.Tests und fügen dann eine neue FakeDinnerRepository-Klasse hinzu (klicken Sie mit der rechten Maustaste auf den Ordner, und wählen Sie "Add-New> Class" aus):

Screenshot des Menüelements

Wir aktualisieren den Code so, dass die FakeDinnerRepository-Klasse die IDinnerRepository-Schnittstelle implementiert. Anschließend können wir mit der rechten Maustaste darauf klicken und den Kontextmenübefehl "Schnittstelle IDinnerRepository implementieren" auswählen:

Screenshot des Befehls im Kontextmenü

Dadurch fügt Visual Studio automatisch alle IDinnerRepository-Schnittstellenmember zu unserer FakeDinnerRepository-Klasse mit standardmäßigen "Stub out"-Implementierungen hinzu:

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();
    }
}

Anschließend können wir die FakeDinnerRepository-Implementierung aktualisieren, um mit einer in-Memory-Liste<Dinner-Sammlung> zu arbeiten, die ihr als Konstruktorargument übergeben wird.

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");
        }
    }
}

Wir haben jetzt eine gefälschte IDinnerRepository-Implementierung, die keine Datenbank erfordert, und kann stattdessen eine Speicherliste von Dinner-Objekten ausarbeiten.

Verwenden des FakeDinnerRepository mit Unit Tests

Kehren wir zu den DinnersController-Komponententests zurück, die zuvor fehlgeschlagen sind, da die Datenbank nicht verfügbar war. Wir können die Testmethoden aktualisieren, um ein FakeDinnerRepository zu verwenden, das mit Beispieldaten für Dinner im Speicher gefüllt ist und im DinnersController mit dem folgenden Code verwendet wird:

[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);
    }
}

Und jetzt, wenn wir diese Tests ausführen, bestehen beide:

Screenshot der Komponententests, beide Tests wurden bestanden.

Am besten ist, dass sie in nur einem Bruchteil einer Sekunde ausgeführt werden und keine komplizierte Einrichtungs- oder Bereinigungslogik erfordern. Wir können jetzt den gesamten Action-Methodencode "DinnersController" (einschließlich Auflistung, Paging, Details, Erstellen, Aktualisieren und Löschen) testen, ohne eine Verbindung mit einer echten Datenbank herstellen zu müssen.

Nebenthema: Dependency Injection Frameworks
Die Manuelle Abhängigkeitsinjektion (wie oben beschrieben) funktioniert einwandfrei, wird jedoch schwieriger zu verwalten, da die Anzahl der Abhängigkeiten und Komponenten in einer Anwendung zunimmt. Für .NET gibt es mehrere Abhängigkeitsinjektionsframeworks, die ihnen helfen können, noch mehr Flexibilität bei der Abhängigkeitsverwaltung zu bieten. Diese Frameworks, auch als "Inversion of Control" (IoC)-Container bezeichnet, stellen Mechanismen bereit, die eine zusätzliche Konfigurationsunterstützung für das Angeben und Übergeben von Abhängigkeiten an Objekte zur Laufzeit ermöglichen (am häufigsten mithilfe der Konstruktoreinfügung). Einige der beliebtesten OSS Dependency Injection / IOC Frameworks in .NET sind: AutoFac, Ninject, Spring.NET, StructureMap und Windsor. ASP.NET MVC macht Erweiterbarkeits-APIs verfügbar, mit denen Entwickler an der Auflösung und Instanziierung von Controllern teilnehmen können und Dependency Injection /IoC-Frameworks in diesem Prozess sauber integriert werden können. Die Verwendung eines DI/IOC-Frameworks würde es uns auch ermöglichen, den Standardkonstruktor aus unserem DinnersController zu entfernen – was die Kopplung zwischen diesem und dem DinnerRepository vollständig entfernen würde. Wir verwenden kein Abhängigkeitsinjektions-/IOC-Framework mit unserer NerdDinner-Anwendung. Aber es ist etwas, das wir für die Zukunft in Betracht ziehen könnten, wenn die NerdDinner-Codebasis und -fähigkeiten gewachsen sind.

Erstellen von Unit-Tests für Bearbeitungsaktionen

Nun erstellen wir einige Komponententests, mit denen die Bearbeitungsfunktion des DinnersController überprüft wird. Wir testen zunächst die HTTP-GET Version unserer Bearbeitungsaktion:

//
// 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));
}

Wir erstellen einen Test, der überprüft, ob eine von einem DinnerFormViewModel-Objekt unterstützte Ansicht wieder gerendert wird, wenn eine gültige Dinner-Anfrage gemacht wird.

[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));
}

Wenn der Test ausgeführt wird, stellen wir jedoch fest, dass er fehlschlägt, da eine Null-Verweis-Ausnahme ausgelöst wird, wenn die Edit-Methode auf die User.Identity.Name-Eigenschaft zugreift, um die Überprüfung mit Dinner.IsHostedBy() durchzuführen.

Das User -Objekt auf der Controller-Basisklasse kapselt Details zum angemeldeten Benutzer und wird von ASP.NET MVC aufgefüllt, wenn der Controller zur Laufzeit erstellt wird. Da wir den DinnersController außerhalb einer Webserverumgebung testen, wird das User-Objekt nicht festgelegt (daher die Null-Verweis-Ausnahme).

Mocken der User.Identity.Name-Eigenschaft

Simulierte Frameworks vereinfachen das Testen, indem wir dynamisch gefälschte Versionen abhängiger Objekte erstellen können, die unsere Tests unterstützen. Beispielsweise können wir in unserem Bearbeiten-Aktionstest ein Mocking-Framework verwenden, um dynamisch ein User-Objekt zu erstellen, das unser DinnersController verwenden kann, um einen simulierten Benutzernamen zu suchen. Dadurch wird verhindert, dass beim Ausführen des Tests ein Nullverweis ausgelöst wird.

Es gibt viele .NET-Mocking-Frameworks, die mit ASP.NET MVC verwendet werden können (eine Liste dieser Frameworks finden Sie hier: http://www.mockframeworks.com/).

Nach dem Herunterladen fügen wir der Moq.dll-Assembly einen Verweis in unserem Projekt "NerdDinner.Tests" hinzu:

Screenshot des Navigationsbaums

Anschließend fügen wir unserer Testklasse eine Hilfsmethode "CreateDinnersControllerAs(username)" hinzu, die einen Benutzernamen als Parameter akzeptiert und dann die User.Identity.Name-Eigenschaft in der DinnersController-Instanz "simuliert":

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;
}

Oben verwenden wir Moq, um ein Mock-Objekt zu erstellen, das ein ControllerContext-Objekt fälscht (was ASP.NET MVC an Controller-Klassen übergibt, um Laufzeitobjekte wie User, Request, Response und Session zur Verfügung zu stellen). Wir rufen die "SetupGet"-Methode für das Mock auf, um anzugeben, dass die HttpContext.User.Identity.Name-Eigenschaft auf ControllerContext die Benutzernamenzeichenfolge zurückgeben soll, die wir an die Hilfsmethode übergeben haben.

Wir können eine beliebige Anzahl von ControllerContext-Eigenschaften und -Methoden modellieren. Um dies zu veranschaulichen, habe ich auch einen SetupGet()-Aufruf für die Request.IsAuthenticated-Eigenschaft hinzugefügt (was für die folgenden Tests nicht erforderlich ist – was aber hilft, zu veranschaulichen, wie Sie Anforderungseigenschaften simuliert). Wenn wir fertig sind, weisen wir eine Instanz des ControllerContext-Mock dem DinnersController zu, den unsere Hilfsmethode zurückgibt.

Wir können jetzt Komponententests schreiben, die diese Hilfsmethode verwenden, um Bearbeitungsszenarien mit unterschiedlichen Benutzern zu testen:

[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");
}

Und jetzt, wenn wir die Tests ausführen, die sie bestehen:

Screenshot der Komponententests, die Hilfsmethode verwenden. Die Tests wurden bestanden.

Testen von UpdateModel()-Szenarien

Wir haben Tests erstellt, die die HTTP-GET Version der Bearbeitungsaktion abdecken. Nun erstellen wir einige Tests, mit denen die HTTP-POST Version der Bearbeitungsaktion überprüft wird:

//
// 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));
    }
}

Das interessante neue Testszenario für uns, das mit dieser Aktionsmethode unterstützt wird, ist die Verwendung der UpdateModel()-Hilfsmethode für die Controller-Basisklasse. Wir verwenden diese Hilfsmethode, um Formularpostwerte an unsere Dinner-Objektinstanz zu binden.

Nachfolgend finden Sie zwei Tests, die zeigen, wie wir Formularwerte für die Verwendung in der Hilfsmethode UpdateModel() bereitstellen können. Dazu erstellen und füllen wir ein FormCollection-Objekt und weisen es dann der Eigenschaft "ValueProvider" für den Controller zu.

Der erste Test überprüft, ob bei einem erfolgreichen Speichervorgang der Browser zur Detailansicht umgeleitet wird. Der zweite Test überprüft, ob beim Posten einer ungültigen Eingabe die Bearbeitungsansicht erneut mit einer Fehlermeldung angezeigt wird.The second test verifies that when invalid input is posted the action redisplays the edit view again with an error message.

[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");
}

Testabschluss

Wir haben die Kernkonzepte behandelt, die in Komponententestcontrollerklassen involviert sind. Wir können diese Techniken verwenden, um ganz einfach Hunderte einfacher Tests zu erstellen, die das Verhalten unserer Anwendung überprüfen.

Da unsere Controller- und Modelltests keine echte Datenbank erfordern, sind sie extrem schnell und einfach auszuführen. Wir können Hunderte automatisierter Tests in Sekunden ausführen und sofort Feedback dazu erhalten, ob eine von uns vorgenommene Änderung etwas beschädigt hat. Dies wird uns helfen, unsere Anwendung kontinuierlich zu verbessern, umzugestalten und zu verfeinern.

Wir haben tests als letztes Thema in diesem Kapitel behandelt – aber nicht, weil Das Testen etwas ist, das Sie am Ende eines Entwicklungsprozesses tun sollten! Im Gegenteil, Sie sollten automatisierte Tests so früh wie möglich in Ihrem Entwicklungsprozess schreiben. Auf diese Weise können Sie während der Entwicklung sofort Feedback bekommen, durchdacht über die Anwendungsfallszenarien nachdenken und Ihre Anwendung mit sauberer Schichtung und Kopplung entwerfen.

In einem späteren Kapitel des Buches wird die Test Driven Development (TDD) und die Verwendung mit ASP.NET MVC erläutert. TDD ist eine iterative Codierungspraxis, in der Sie zuerst die Tests schreiben, die vom resultierenden Code erfüllt werden. Mit TDD beginnen Sie mit jedem Feature, indem Sie einen Test erstellen, der die Funktionalität überprüft, die Sie implementieren möchten. Wenn Sie zuerst den Komponententest schreiben, können Sie sicherstellen, dass Sie das Feature klar verstehen und wie es funktionieren soll. Erst nachdem der Test geschrieben wurde (und Sie überprüft haben, dass er fehlschlägt), implementieren Sie dann die tatsächliche Funktionalität, die der Test überprüft. Da Sie bereits Zeit damit verbracht haben, über den Anwendungsfall nachzudenken, wie das Feature funktionieren soll, haben Sie ein besseres Verständnis der Anforderungen und wie sie am besten implementiert werden können. Wenn Sie mit der Implementierung fertig sind, können Sie den Test erneut ausführen – und erhalten sofortiges Feedback darüber, ob das Feature ordnungsgemäß funktioniert. Wir werden TDD ausführlicher in Kapitel 10 behandeln.

Nächster Schritt

Einige abschließende Bemerkungen.