Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
von Microsoft
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:
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:
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".
Wenn wir auf die Schaltfläche "OK" klicken, fügt Visual Studio dem Projekt eine DinnerTest.cs Datei hinzu (und öffnet sie):
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:
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 "
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:
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:
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.
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):
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:
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:
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:
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:
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.