Freigeben über


Aktivieren von automatisierten Komponententests

von Microsoft

PDF herunterladen

Dies ist Schritt 12 eines kostenlosen "NerdDinner"-Anwendungstutorial , das das Erstellen einer kleinen, aber vollständigen Webanwendung mit ASP.NET MVC 1 exemplarische Schritte durchläuft.

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

Wenn Sie ASP.NET MVC 3 verwenden, empfiehlt es sich, die Tutorials Erste Schritte Mit MVC 3 oder MVC Music Store zu befolgen.

NerdDinner Schritt 12: Komponententests

Entwickeln wir eine Reihe von automatisierten Komponententests, die unsere NerdDinner-Funktionalität überprüfen und uns das Vertrauen geben, in Zukunft Änderungen und Verbesserungen an der Anwendung vorzunehmen.

Warum Komponententest?

Auf der Fahrt in die Arbeit eines Morgens haben Sie einen plötzlichen Inspirationsblitz für eine Anwendung, an der Sie arbeiten. Sie erkennen, dass es eine Änderung gibt, die Sie implementieren können, die die Anwendung erheblich verbessert. Es kann sich um ein Umgestalten handeln, das den Code bereinigt, ein neues Feature hinzufügt oder einen Fehler behebt.

Die Frage, mit der Sie konfrontiert werden, wenn Sie ihren Computer erreichen, lautet: "Wie sicher ist es, diese Verbesserung vorzunehmen?" Was ist, wenn die Änderung Nebenwirkungen hat oder etwas unterbricht? Die Änderung ist möglicherweise einfach und dauert nur wenige 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?

Automatisierte Komponententests können ein Sicherheitsnetz bereitstellen, mit dem Sie Ihre Anwendungen kontinuierlich verbessern können und vermeiden, Angst vor dem Code zu haben, an dem Sie arbeiten. Mit automatisierten Tests, die die Funktionalität schnell überprüfen, können Sie zuverlässig programmieren – und Sie können Verbesserungen vornehmen, die Sie sich sonst möglicherweise nicht wohl gefühlt hätten. Sie helfen auch bei der Erstellung von Lösungen, die wartungsfreundlicher sind und eine längere Lebensdauer haben - was zu einem viel höheren Return on Investment führt.

Das ASP.NET MVC Framework macht es einfach und natürlich, die Komponententestfunktionalität der Anwendung zu testen. Außerdem wird ein TDD-Workflow (Test Driven Development) aktiviert, der testbasierte Entwicklung ermöglicht.

Projekt NerdDinner.Tests

Als wir zu Beginn dieses Tutorials unsere NerdDinner-Anwendung erstellt haben, wurden wir mit einem Dialog gefragt, ob wir ein Komponententestprojekt für das Anwendungsprojekt erstellen möchten:

Screenshot des Dialogfelds

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

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

Das NerdDinner.Tests-Projekt verweist auf die Projektassembly der NerdDinner-Anwendung und ermöglicht es uns, ihr problemlos automatisierte Tests hinzuzufügen, die die Anwendungsfunktionalität überprüfen.

Erstellen von Komponententests für unsere Dinner-Modellklasse

Fügen Wir unserem Projekt NerdDinner.Tests einige Tests hinzu, die die Dinner-Klasse überprüfen, die wir beim Erstellen unserer Modellebene erstellt haben.

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

Wir erstellen einen "Komponententest" und nennen ihn "DinnerTest.cs":

Screenshot des Dialogfelds

Wenn Sie 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 Code für die Kesselplatte, die ich ein wenig unordentlich finde. Lassen Sie uns sauber, um nur den folgenden Code zu enthalten:

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 für unseren Dinner-Kurs hinzufügen. Der erste Test überprüft, ob unser Dinner ungültig ist, wenn ein neues Dinner erstellt wird, ohne dass alle Eigenschaften richtig festgelegt wurden. Der zweite Test überprüft, ob unser Dinner gültig ist, wenn für ein Dinner alle Eigenschaften mit gültigen Werten festgelegt sind:

[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 von kleinen Tests erstellen und es einfach machen möchten, die Absicht und das Verhalten der einzelnen Tests schnell zu bestimmen (insbesondere, wenn wir eine Liste von Fehlern in einem Testrunner durchsehen). Die Testnamen sollten nach der Funktionalität benannt werden, die sie testen. Oben verwenden wir ein "Noun_Should_Verb"-Benennungsmuster.

Wir strukturieren die Tests mit dem Testmuster "AAA", das für "Arrange, Act, Assert" steht:

  • Anordnen: Einrichten der getesteten Einheit
  • Act: Übung der Testeinheit und Erfassung der Ergebnisse
  • Assert: Überprüfen des Verhaltens

Beim Schreiben von Tests möchten wir vermeiden, dass die einzelnen Tests zu viel tun. Stattdessen sollte jeder Test nur ein einzelnes Konzept überprüfen (was es viel einfacher macht, die Ursache von Fehlern zu ermitteln). Eine gute Richtlinie besteht darin, nur eine einzelne Assert-Anweisung für jeden Test zu verwenden. Wenn Sie über mehr als eine Assert-Anweisung in einer Testmethode verfügen, stellen Sie sicher, dass sie alle zum Testen desselben Konzepts verwendet werden. Führen Sie im Zweifelsfall einen weiteren Test durch.

Tests werden ausgeführt

Visual Studio 2008 Professional (und höhere Editionen) enthält einen integrierten Testrunner, mit dem Visual Studio Unit Test-Projekte innerhalb der IDE ausgeführt werden können. Wir können den Befehl Test-Run-All>> Tests im Menü Projektmappe auswählen (oder STRG R, A eingeben), um alle Komponententests auszuführen. Alternativ können wir den Cursor in einer bestimmten Testklasse oder Testmethode positionieren und den Befehl Test-Run-Tests>> im Aktuellen Kontextmenü verwenden (oder STRG R, T eingeben), um eine Teilmenge der Komponententests auszuführen.

Positionieren Sie den Cursor in der DinnerTest-Klasse, und geben Sie "STRG R, T" ein, um die beiden soeben definierten Tests auszuführen. In diesem Fall wird in Visual Studio ein Fenster "Testergebnisse" angezeigt, in dem die Ergebnisse unserer Testausführung aufgeführt werden:

Screenshot des Fensters

Hinweis: Im Vs-Testergebnisfenster wird die Spalte Klassenname standardmäßig nicht angezeigt. Sie können dies hinzufügen, indem Sie mit der rechten Maustaste im Fenster Testergebnisse klicken und den Menübefehl Spalten hinzufügen/entfernen verwenden.

Die Ausführung unserer beiden Tests dauerte nur einen Bruchteil einer Sekunde – und wie Sie sehen, haben beide bestanden. Wir können sie nun erweitern, indem wir zusätzliche Tests erstellen, die bestimmte Regelvalidierungen überprüfen und die beiden Hilfsmethoden – IsUserHost() und IsUserRegistered() – abdecken, die wir der Dinner-Klasse hinzugefügt haben. Wenn alle diese Tests für den Dinner-Kurs eingerichtet werden, wird es in Zukunft viel einfacher und sicherer sein, neue Geschäftsregeln und Validierungen hinzuzufügen. Wir können Dinner unsere neue Regellogik hinzufügen und dann innerhalb von Sekunden überprüfen, ob keine unserer vorherigen Logikfunktionen beschädigt wurde.

Beachten Sie, dass die Verwendung eines aussagekräftigen Testnamens es leicht macht, schnell zu verstehen, was jeder Test überprüft. Ich empfehle, den Menübefehl Extras-Optionen> zu verwenden, den Test Tools-Testausführungskonfigurationsbildschirm> zu öffnen und das Kontrollkästchen "Doppelklicken auf ein fehlerhaftes oder nicht schlüssiges Komponententestergebnis zeigt den Fehlerpunkt im Test an". Dadurch können Sie auf einen Fehler im Testergebnisfenster doppelklicken und sofort zum Assert-Fehler springen.

Erstellen von DinnersController-Komponententests

Lassen Sie uns nun einige Komponententests erstellen, die unsere DinnersController-Funktionalität überprüfen. Klicken Sie zunächst mit der rechten Maustaste auf den Ordner "Controller" in unserem Testprojekt, und wählen Sie dann den Menübefehl Add-New> Test aus. Wir erstellen einen "Komponententest" und nennen ihn "DinnersControllerTest.cs".

Wir erstellen zwei Testmethoden, mit denen die Aktionsmethode Details() auf dem DinnersController überprüft wird. Die erste überprüft, ob eine Ansicht zurückgegeben wird, wenn ein vorhandenes Dinner angefordert wird. Mit der zweiten wird überprüft, ob eine "NotFound"-Ansicht zurückgegeben wird, wenn ein nicht vorhandenes Dinner 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 uns die Fehlermeldungen ansehen, sehen wir, dass der Grund, warum die Tests fehlgeschlagen sind, 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 Projekt NerdDinner.Tests in einem anderen Verzeichnis als dem Anwendungsprojekt kompiliert und ausgeführt wird, ist der relative Pfadspeicherort unserer Verbindungszeichenfolge falsch.

Wir könnten dies beheben, indem wir die SQL Express-Datenbankdatei in unser Testprojekt kopieren und dann eine entsprechende Testverbindungszeichenfolge im App.config unseres Testprojekts hinzufügen. Dadurch würden die oben genannten Tests aufgehoben und ausgeführt.

Komponententestcode mit einer echten Datenbank 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 Ihre Komponententests in Sekunden ausgeführt werden können – und sie so selbstverständlich wie das Kompilieren des Projekts.
  • Es erschwert die Setup- und Bereinigungslogik innerhalb von Tests. Jeder Komponententest soll isoliert und unabhängig von anderen (ohne Nebenwirkungen oder Abhängigkeiten) sein. Wenn Sie mit einer echten Datenbank arbeiten, müssen Sie den Zustand beachten und ihn zwischen Tests zurücksetzen.

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

Dependency Injection

Im Moment ist DinnersController eng mit der DinnerRepository-Klasse "gekoppelt". "Kopplung" bezieht sich auf eine Situation, in der eine Klasse explizit von einer anderen Klasse abhängig ist, um zu funktionieren:

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, die die DinnersController-Klasse vom DinnerRepository hat, schließlich eine Datenbank, 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 ermöglichen) nicht mehr implizit in 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. Dadurch 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 ist das Erstellen einer neuen IDinnerRepository-Schnittstelle, die den Repositoryvertrag kapselt, den unsere Controller zum Abrufen und Aktualisieren von Dinners benötigen.

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

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

Screenshot: Im Untermenü

Dadurch wird das Dialogfeld "Schnittstelle extrahieren" gestartet, und wir werden aufgefordert, den Namen der zu erstellenden Schnittstelle einzugeben. Standardmäßig wird IDinnerRepository verwendet und automatisch alle öffentlichen Methoden der vorhandenen DinnerRepository-Klasse 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, sodass die Schnittstelle implementiert wird:

public class DinnerRepository : IDinnerRepository {
   ...
}

Aktualisieren von DinnersController zur Unterstützung der Einschleusung des Konstruktors

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

Derzeit ist DinnersController so hartcodiert, dass 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 es, ein IDinnerRepository als Argument zu übergeben. Der 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 mithilfe von Standardkonstruktoren erstellt, verwendet unser DinnersController zur Laufzeit weiterhin die DinnerRepository-Klasse, um den Datenzugriff durchzuführen.

Wir können jetzt jedoch unsere Komponententests aktualisieren, um eine "gefälschte" Repositoryimplementierung mit dem Parameterkonstruktor zu bestehen. Dieses "gefälschte" Dinnerrepository erfordert keinen Zugriff auf eine echte Datenbank, sondern verwendet stattdessen In-Memory-Beispieldaten.

Erstellen der FakeDinnerRepository-Klasse

Erstellen Wir eine FakeDinnerRepository-Klasse.

Wir beginnen mit dem Erstellen eines "Fakes"-Verzeichnisses innerhalb unseres NerdDinner.Tests-Projekts 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. Wir können dann mit der rechten Maustaste darauf klicken und den Kontextmenübefehl "Schnittstelle implementieren IDinnerRepository" auswählen:

Screenshot des Kontextmenübefehls

Dies führt dazu, dass Visual Studio automatisch alle Elemente der IDinnerRepository-Schnittstelle unserer FakeDinnerRepository-Klasse mit standardmäßigen "Stub out"-Implementierungen hinzu fügt:

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 so aktualisieren, dass sie von einer In-Memory<List Dinner-Auflistung> ausgeht, die als Konstruktorargument an sie ü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 verfügen jetzt über eine gefälschte IDinnerRepository-Implementierung, die keine Datenbank erfordert und stattdessen eine In-Memory-Liste von Dinner-Objekten erstellen kann.

Verwenden von FakeDinnerRepository mit Komponententests

Kehren wir zu den DinnersController-Komponententests zurück, die zuvor fehlgeschlagen sind, weil die Datenbank nicht verfügbar war. Wir können die Testmethoden so aktualisieren, dass sie eine FakeDinnerRepository verwenden, die mit Beispieldaten im Arbeitsspeicher aufgefüllt ist:

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

Wenn wir nun diese Tests ausführen, bestehen beide:

Screenshot der Komponententests, die beide Tests bestanden haben.

Das Beste ist, dass die Ausführung nur einen Bruchteil einer Sekunde benötigt und keine komplizierte Setup-/Bereinigungslogik erfordert. Wir können jetzt den gesamten Code der DinnersController-Aktionsmethode (einschließlich Auflistung, Paging, Details, Erstellen, Aktualisieren und Löschen) komponententesten, ohne jemals eine Verbindung mit einer echten Datenbank herstellen zu müssen.

Nebenthema: Dependency Injection Frameworks
Die manuelle Abhängigkeitsinjektion (wie oben beschrieben) funktioniert gut, ist aber schwieriger zu verwalten, wenn die Anzahl von Abhängigkeiten und Komponenten in einer Anwendung zunimmt. Für .NET gibt es mehrere Frameworks zur Einschleusung von Abhängigkeiten, die noch mehr Flexibilität bei der Abhängigkeitsverwaltung bieten können. Diese Frameworks, manchmal auch als "Inversion of Control" (IoC)-Container bezeichnet, bieten Mechanismen, die eine zusätzliche Ebene der Konfigurationsunterstützung für das Angeben und Übergeben von Abhängigkeiten an Objekte zur Laufzeit ermöglichen (meist mithilfe von Konstruktoreinschleusung). 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, die Es Entwicklern ermöglichen, an der Auflösung und Instanziierung von Controllern teilzunehmen und Dependency Injection/IoC-Frameworks sauber in diesen Prozess zu integrieren. 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 aufheben würde. Wir verwenden kein Abhängigkeitsinjektion/IOC-Framework mit unserer NerdDinner-Anwendung. Aber es ist etwas, das wir für die Zukunft in Betracht ziehen könnten, wenn die Codebasis und die Funktionen von NerdDinner wachsen würden.

Erstellen von Aktionskomponententests bearbeiten

Nun erstellen wir einige Komponententests, die die Bearbeitungsfunktion des DinnersControllers überprüfen. Zunächst testen wir 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 ein von einem DinnerFormViewModel-Objekt gesichertes View-Objekt zurückgespiegelt wird, wenn ein gültiges Abendessen angefordert 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 wir den Test ausführen, werden wir jedoch feststellen, 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 Dinner.IsHostedBy()-Überprüfung durchzuführen.

Das User-Objekt in 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, ist das User-Objekt nicht festgelegt (daher die NULL-Verweisausnahme).

Simulieren der User.Identity.Name-Eigenschaft

Mocking-Frameworks erleichtern das Testen, da wir dynamisch gefälschte Versionen abhängiger Objekte erstellen können, die unsere Tests unterstützen. Beispielsweise können wir ein MockingFramework in unserem Aktionstest Bearbeiten verwenden, um dynamisch ein User-Objekt zu erstellen, das unser DinnersController verwenden kann, um einen simulierten Benutzernamen zu suchen. Dadurch wird vermieden, dass ein NULL-Verweis ausgelöst wird, wenn wir unseren Test ausführen.

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 assembly Moq.dll einen Verweis in unserem Projekt NerdDinner.Tests hinzu:

Screenshot der Navigationsstruktur

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 auf dem DinnersController-instance "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 verfügbar zu machen). Wir rufen die Methode "SetupGet" im Mock auf, um anzugeben, dass die HttpContext.User.Identity.Name-Eigenschaft für ControllerContext die Benutzernamenzeichenfolge zurückgeben soll, die wir an die Hilfsmethode übergeben haben.

Wir können eine beliebige Anzahl von ControllerContext-Eigenschaften und -Methoden simulieren. Um dies zu veranschaulichen, habe ich auch einen SetupGet()-Aufruf für die Request.IsAuthenticated-Eigenschaft hinzugefügt (die eigentlich nicht für die folgenden Tests benötigt wird – was jedoch veranschaulicht, wie Sie Anforderungseigenschaften simulieren können). Wenn wir fertig sind, weisen wir dem DinnersController eine instance der ControllerContext-Mock zu, die von unserer Hilfsmethode zurückgegeben wird.

Wir können jetzt Komponententests schreiben, die diese Hilfsmethode verwenden, um Bearbeitungsszenarien mit verschiedenen 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 die Hilfsmethode verwenden. Die Tests wurden bestanden.

Testen von UpdateModel()-Szenarien

Wir haben Tests erstellt, die die HTTP-GET-Version der Aktion Bearbeiten abdecken. Nun erstellen wir einige Tests, die die HTTP-POST-Version der Aktion Bearbeiten überprüfen:

//
// 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, das wir mit dieser Aktionsmethode unterstützen können, ist die Verwendung der UpdateModel()-Hilfsmethode für die Controller-Basisklasse. Wir verwenden diese Hilfsmethode, um Form-Post-Werte an unser Dinner-Objekt instance zu binden.

Im Folgenden finden Sie zwei Tests, die zeigen, wie wir Formularpostwerte für die zu verwendende UpdateModel()-Hilfsmethode bereitstellen können. Dazu erstellen und füllen Sie ein FormCollection-Objekt und weisen es dann der Eigenschaft "ValueProvider" auf dem Controller zu.

Beim ersten Test wird überprüft, ob der Browser bei einem erfolgreichen Speichern zur Detailaktion umgeleitet wird. Beim zweiten Test wird überprüft, dass die Aktion die Bearbeitungsansicht erneut mit einer Fehlermeldung zeigt, wenn eine ungültige Eingabe gesendet wird.

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

Testen Wrap-Up

Wir haben die wichtigsten Konzepte behandelt, die mit Komponententestcontrollerklassen verbunden sind. Wir können diese Techniken verwenden, um problemlos Hunderte von einfachen 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 werden in der Lage sein, Hunderte von automatisierten Tests in Sekunden auszuführen und sofort Feedback zu erhalten, ob eine Änderung, die wir vorgenommen haben, etwas kaputt ging. Dies gibt uns das Vertrauen, unsere Anwendung kontinuierlich zu verbessern, umzugestalten und zu verfeinern.

Als letztes Thema in diesem Kapitel haben wir Das Testen behandelt – aber nicht, weil sie am Ende eines Entwicklungsprozesses testen sollten! Im Gegenteil, Sie sollten automatisierte Tests so früh wie möglich in Ihrem Entwicklungsprozess schreiben. Auf diese Weise erhalten Sie während der Entwicklung sofortiges Feedback, helfen Ihnen dabei, die Anwendungsfallszenarien Ihrer Anwendung nachzudenken, und leitet Sie zum Entwerfen Ihrer Anwendung mit sauber Schichtung und Kopplung.

In einem späteren Kapitel des Buches werden Test Driven Development (TDD) und deren Verwendung mit ASP.NET MVC erläutert. TDD ist eine iterative Codierungsmethode, bei der Sie zuerst die Tests schreiben, die ihr resultierender Code erfüllt. 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, stellen Sie sicher, dass Sie das Feature und die Funktionsweise klar verstehen. Erst nachdem der Test geschrieben wurde (und Sie überprüft haben, dass er fehlschlägt), implementieren Sie die tatsächliche Funktionalität, die der Test überprüft. Da Sie bereits über den Anwendungsfall nachgedacht haben, wie das Feature funktionieren soll, haben Sie ein besseres Verständnis der Anforderungen und deren implementierung. Wenn Sie mit der Implementierung fertig sind, können Sie den Test erneut ausführen und sofortiges Feedback erhalten, ob das Feature ordnungsgemäß funktioniert. Weitere Informationen zu TDD finden Sie in Kapitel 10.

Nächster Schritt

Einige abschließende Kommentare.