ASP.NET

Komponententests in der Navigation für ASP.NET Web Forms-Framework

Graham Mendick

Library herunterladen

Das Navigation for ASP.NET Web Forms-Framework, ein unter navigation.codeplex.com gehostetes Open-Source-Projekt, eröffnet durch einen neuen Ansatz zu Navigation und Datenübergabe neue Möglichkeiten für das Programmieren von Anwendungen für Webformulare. In klassischen Webformularen ist die Art und Weise der Datenübergabe von der Navigation abhängig. Die Information kann z. B. während einer Umleitung im Such-String oder den Routing-Daten enthalten sein, während einer Rückgabe jedoch in den Kontrollwerten oder dem Ansichtsstatus. In der Navigation des ASP.NET Web Forms Framework (im Folgenden kurz “Navigation Framework” genannt) wird für sämtliche Szenarien eine einzige Datenquelle verwendet.

In meinem ersten Artikel (msdn.microsoft.com/magazine/hh975349) habe ich das Navigation Framework vorgestellt und am Beispiel einer Anwendung für eine Online-Umfrage einige der Schlüsselkonzepte und deren Vorteile erläutert. Ich habe insbesondere gezeigt, wie es die Erzeugung eines Sets von dynamischen, kontext-sensitiven Breadcrumb-Hyperlinks einem Nutzer ermöglicht, zu einer zuvor aufgerufenen Frage samt gespeicherter Nutzer-Antwort zurückzukehren, so dass die Einschränkungen der statischen ASP.NET Site Map Funktionalität überwunden werden.

In meinem ersten Artikel habe ich die These aufgestellt, dass Sie mithilfe des Navigation Framework Webformular-Code verfassen können, der eine ASP.NET MVC-Anwendung vor Neid erblassen lässt. Die Beispielanwendung (Online-Umfrage) konnte diese Vermutung jedoch nicht bestätigen, da der Code hinter zahlreichen Codebehinds versteckt war und die einzelnen Einheiten unüberschaubar wurden.

Ich werde dies nun in diesem zweiten Artikel klarstellen und die Umfrage-Anwendung so editieren, dass sie ebenso gut strukturiert ist wie eine typische ASP.NET MVC-Anwendung und sogar noch über eine höhere Ebene der Komponententests verfügt. Ich werde die ASP.NET Datenbindung zusammen mit dem Navigations-Rahmenwerk verwenden, um die Codebehinds zu löschen und die Geschäftslogik in eine separate Klasse auszulagern, mit der dann der Komponententest durchgeführt wird. Dieser Test wird kein "Mocking" erfordern und die Codeabdeckung wird die Navigationslogik einschließen—diese Funktionen werden selten von ASP.NET Komponententests zur Verfügung gestellt.

Datenbindung

Der Friedhof für Webformularcode ist überhäuft mit aufgeblähten Leichen von Codebehinddateien, doch dies muss nicht so sein. Obwohl Webformulare von Beginn an über die Funktion der Datenbindung verfügen, hat Visual Studio erst im Jahr 2005 Datenquellen-Steuerelemente sowie die Bindesyntax für eine bidirektionale Datenbindung mit Updates eingeführt, und so die Entwicklung von Webformularanwendungen mehr der Struktur einer typischen MVC-Anwendung angeglichen. Die positiven Auswirkungen eines solchen Codes, insbesondere im Hinblick auf Komponententests, sind weithin anerkannt und spiegeln sich auch in der Tatsache wider, dass der Großteil der Bemühungen in der Entwicklung von Webformularen für die nächste Version von Visual Studio in diesem Bereich stattfand.

Um dies zu zeigen, greife ich das Beispiel der Anwendung für die Online-Umfrage aus dem ersten Artikel wieder auf und konvertiere diese in eine Architektur, die MVC vergleichbar ist. Eine Kontrollerklasse soll die Geschäftslogik und enthalten und die ViewModel-Klassen sollen die Kommunikationsdaten zwischen Kontroller und Ansichten enthalten. Hierzu ist sehr wenig Entwicklungsaufwand erforderlich, da der gegenwärtige Code in den Codebehinds kann mittels "Cut & Paste" nahezu wortgetreu in den Kontroller eingefügt werden.

Fängt man mit Question1.aspx an, so erzeugt man im ersten Schritt eine Question ViewModel-Klasse mit einer String-Eigenschaft, sodass die ausgewählte Antwort dem Kontroller zugespielt und von diesem zurückgespielt werden kann:

public class Question
{
  public string Answer
  {
    get;
    set;
  }
}

Als nächstes ist die Kontroller-Klasse an der Reihe, die ich SurveyController nenne; im Unterschied zu einem MVC-Kontroller handelt es sich um ein Plain Old CLR-Objekt. Question1.aspx benötigt zwei Methoden, eine für den Datenabruf, der die Question ViewModel-Klasse zurückgibt, und eine für den Updatevorgang, der die Question ViewModel-Klasse akzeptiert:

public class SurveyController
{
  public Question GetQuestion1()
  {
    return null;
  }
  public void UpdateQuestion1(Question question)
  {
  }
}

Um diese Methoden zu erarbeiten, verwende ich den Code im Codebehind von Question1.aspx und verändere die Seitenladelogik in GetQuestion1 und the Click-Handlerlogik in UpdateQuestion1. Da der Kontroller keinen Zugang zu den Kontrollen auf der Seite hat, wird die Question ViewModel-Klasse dazu verwendet, um die eher die Antwort als die Optionsfeldliste zu erhalten und festzulegen. Die GetQuestion1-Methode erfordert eine weitere Optimierung, damit sichergestellt werden kann, dass die zurückgegebene Default-Antwort "Webformular" ist:

public Question GetQuestion1()
{
  string answer = "Web Forms";
  if (StateContext.Data["answer"] != null)
  {
    answer = (string)StateContext.Data["answer"];
  }
  return new Question() { Answer = answer };
}

In MVC findet die Datenbindung auf der Anforderungsebene statt, wo die Anforderung über Route Registration einer Controller Methode zugeordnet ist, in Webformularen hingegen ist die Datenbindung der Kontrollebene zugeordnet, wobei die Zuordnung durch Nutzung einer ObjectDataSource gegeben ist. Um also Question1.aspx den SurveyController-Methoden zuzuordnen, füge ich einen FormView, der mit einer entsprechend konfigurierten Datenquelle verbunden ist, hinzu.

<asp:FormView ID="Question" runat="server"
  DataSourceID="QuestionDataSource" DefaultMode="Edit">
  <EditItemTemplate>
  </EditItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID="QuestionDataSource" 
  runat="server" SelectMethod="GetQuestion1" 
  UpdateMethod="UpdateQuestion1" TypeName="Survey.SurveyController"  
  DataObjectTypeName="Survey.Question" />

Zuletzt wird die Frage, die aus der Optionsfeldliste und Schaltfläche besteht, in das EditItemTemplate von FormView eingefügt. Gleichzeitig müssen zwei Veränderungen vorgenommen werden, damit die Datenbindemechanismen funktionieren. Erstens muss die Bindesyntax so verwendet werden, dass die von GetQuestion1 zurückgegebene Antwort angezeigt wird und die neu ausgewählte Antwort zu UpdateQuestion1 zurückgespielt wird. Zweitens muss der CommandName der Schaltfläche auf Update gesetzt sein, so dass UpdateQuestion1 automatisch bei Anklicken des Buttons aufgerufen wird (Sie werden bemerken, dass das ausgewählte Attribut der ersten Objektliste gelöscht wurde, da das Auswählen einer Default-Antwort im "Webformular" nun in GetQuestion1 verwaltet wird):

<asp:RadioButtonList ID="Answer" runat="server"
  SelectedValue='<%# Bind("Answer") %>'>
  <asp:ListItem Text="Web Forms" />
  <asp:ListItem Text="MVC" />
</asp:RadioButtonList>
<asp:Button ID="Next" runat="server" 
  Text="Next" CommandName="Update" />

Damit ist nun der Prozess für Question1.aspx vollständig, und der Codebehind ist glücklicherweise leer. Dieselben Schritte sind erforderlich um Datenbindung zu Question2.aspx hinzuzufügen, jedoch kann dessen Codebehind nicht komplett gelöscht werden, da der Seitenladecode der Rückwärtsnavigations-Hyperlinks dort für diese Zeit verbleiben muss. Im nächsten Abschnitt, in dem die Einbindung des Navigation Rahmenwerks in Datenbindung vorgestellt wird, wird dieser in das Markup geschrieben und der Codebehind ist leer.

Das Hinzufügen von Datenbindung zu Thanks.aspx erfolgt auf ähnliche Weise, statt jedoch die ungeeignet benannte Question ViewModel-Klasse wiederzuverwenden, erzeuge ich eine neue Klasse mit Namen "Summary", die eine String-Eigenschaft besitzt, um die ausgewählten Antworten speichern zu können:

public class Summary
{
  public string Text
  {
    get;
    set;
  }
}

Da es sich bei Thanks.aspx um einen schreibgeschützten Bildschirm handelt, ist auf dem Kontroller nur eine Datenabrufmethode erforderlich, und, ebenso wie bei Question2.aspx kann der komplette Seitenladecode von der Rückwärtsnavigationslogik in diese Methode geschrieben werden:

public Summary GetSummary()
{
  Summary summary = new Summary();
  summary.Text = (string)StateContext.Data["technology"];
  if (StateContext.Data["navigation"] != null)
  {
    summary.Text += ", " + (bool)StateContext.Data["navigation"];
  }
  return summary;
}

Da keine Update-Funktionalität erforderlich ist, wird das FormView ItemTemplate anstelle des EditItemTemplate verwendet, und die Syntax für die unidirektionale Bindung, Eval, wird anstelle der Bindung verwendet:

<asp:FormView ID="Summary" runat="server" 
  DataSourceID="SummaryDataSource">
  <ItemTemplate>
    <asp:Label ID="Details" runat="server" 
      Text='<%# Eval("Text") %>' />
  </ItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID="SummaryDataSource" 
  runat="server"
  SelectMethod="GetSummary" 
  TypeName="Survey.SurveyController" />

Die halbe Schlacht der Komponententests ist gewonnen, da die Geschäftslogik der Umfrage-Anwendung in eine separate Klasse ausgelagert wurde. Da der Code jedoch virtuell in den Controller eingefügt wurde und vom Codebehind unverändert geblieben ist, sind die Potenziale der Datenbindung noch nicht komplett ausgeschöpft.

Datenbindung für Navigation

Der Code der Umfrage-Anwendung weist noch immer eine Reihe von Problemen auf: Nur die Update-Methoden im SurveyController sollten die Navigationslogik enthalten, und die Codebehinds sollten leer sein. Komponententests sollten nicht begonnen werden, bevor diese Probleme gelöst sind, da die ersten unnötig komplex ausfallen würden und letztere unter Umständen nicht 100% Testergebnis abdecken würden.

Die ausgewählten Parameter der Datenquellen-Steuerungselemente machen ein Zugreifen auf das HttpRequest-Objekt in datengebundenen Methoden überflüssig. Zum Beispiel gestattet es die QueryStringParameter-Klasse, dass Abfragezeichenfolgen als Parameter auf datengebundene Methoden übergeben werden. Das Navigation Framework besitzt eine NavigationDataParameter-Klasse, die einen äquivalenten Job für die statischen Daten auf dem StateContext-Objekt ausführt.

Ausgestattet mit diesem NavigationDataParameter kann ich nun GetQuestion1 wieder aufsuchen und sämtlichen Code entfernen, der auf die statischen Daten zugreift, indem er die Antwort in einen Methodenparameter verwandelt. Dies vereinfacht den Code erheblich:

public Question GetQuestion1(string answer)
{
  return new Question() { Answer = answer ?? "Web Forms" };
}

Die damit verbundene Änderung von Question1.aspx besteht aus dem Hinzufügen des NavigationDataParameter zu seiner Datenquelle. Damit geht als erstes eine Registrierung des Navigation Namespace oben auf der Seite einher:

<%@ Register assembly="Navigation" 
                       namespace="Navigation" 
                        tagprefix="nav" %>

Anschließend kann der NavigationDataParameter zu den ausgewählten Parametern der Datenquelle hinzugefügt werden:

<asp:ObjectDataSource ID="QuestionDataSource" runat="server"
  SelectMethod="GetQuestion1" UpdateMethod="UpdateQuestion1" 
  TypeName="Survey.SurveyController" 
  DataObjectTypeName="Survey.Question" >
  <SelectParameters>
    <nav:NavigationDataParameter Name="answer" />
  </SelectParameters>
</asp:ObjectDataSource>

Die GetQuestion1-Methode, die von sämtlichem Web-spezifischem Code befreit wurde, eignet sich nun für Komponententests. Das Gleiche kann für GetQuestion2 getan werden.

Für die GetSummary-Methode werden zwei Parameter benötigt, eins für jede Antwort. Das zweite Parameter ist ein Bool, das überprüft, wie die Daten von UpdateQuestion2 übertragen werden, und es muss auf Null gesetzt werden können, da die zweite Frage nicht immer gestellt wird:

public Summary GetSummary(string technology, bool? navigation)
{
  Summary summary = new Summary();
  summary.Text = technology;
  if (navigation.HasValue)
  {
    summary.Text += ", " + navigation.Value;
  }
  return summary;
}

Und die entsprechende Änderung an der Datenquelle von Thanks.aspx besteht aus dem Hinzufügen der zwei NavigationDataParameter:

<asp:ObjectDataSource ID="SummaryDataSource" runat="server"
  SelectMethod="GetSummary" TypeName="Survey.SurveyController" >
  <SelectParameters>
    <nav:NavigationDataParameter Name="technology" />
    <nav:NavigationDataParameter Name="navigation" />
  </SelectParameters>
</asp:ObjectDataSource>

Das erste Problem bezüglich des Codes der Umfrage-Anwendung wurde behandelt, da nur die Update-Methoden im Kontroller Navigationslogik enthalten.

Sie erinnern sich, dass das Navigation Framework durch die statische Breadcrumb-Navigationsfunktionalität verbessert wird, die durch die Sitemap der Webformulare bereitgestellt wird; diese protokolliert die besuchten Zustände zusammen mit den Statusdaten und erstellt daraus einen kontextsensitiven Breadcrumb-Pfad der Route, die aktuell durch den User gewählt wurde. Für das Erstellen der Hyperlinks der Rückwärtsnavigation als Markup—ohne Codebehind—bietet das Navigation Framework analog zur SiteMapPath-Kontrolle eine CrumbTrailDataSource. Wenn sie als dahinterliegende Datenquelle für eine ListView verwendet wird, gibt die CrumbTrailDataSource eine Liste von Objekten zurück, eine je zuvor besuchtem Zustand, wobei jede eine NavigationLink URL enthält, die eine kontextsensitive Navigation zurück zu diesem Zustand ermöglicht.

Ich verwende diese neue Datenquelle um die Question2.aspx-Rückwärtsnavigation in deren Markup zu verschieben. Zunächst füge ich eine ListView hinzu, die mit der CrumbTrailDataSource verbunden ist:

<asp:ListView ID="Crumbs" runat="server" 
  DataSourceID="CrumbTrailDataSource">
  <LayoutTemplate>
    <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
  </LayoutTemplate>
  <ItemTemplate>
  </ItemTemplate>
</asp:ListView>
<nav:CrumbTrailDataSource ID="CrumbTrailDataSource" runat="server" />

Als nächstes lösche ich den Seitenladecode vom Question2.aspx Code­behind, schiebe den Hyperlink der Rückwärtsnavigation in das ListView ItemTemplate und verwende die Eval-Bindung um die NavigateUrl-Eigenschaft zu füllen:

<asp:HyperLink ID="Question1" runat="server"
  NavigateUrl='<%# Eval("NavigationLink") %>' Text="Question 1"/>

Sie werden bemerken, dass die HyperLink Text-Eigenschaft fest mit "Frage 1" kodiert ist. Dies funktioniert ebenfalls gut für Question2.aspx, die die einzige Möglichkeit der Rückwärtsnavigation auf die erste Frage verweist. Dies gilt jedoch nicht für Thanks.aspx, da man entweder zur ersten oder zur zweiten Frage zurückkehren kann. Glücklicherweise gestattet es die in der StateInfo.config-Datei eingegebene Navigationskonfiguration, dass ein Attribut mit jedem Zustand assoziiert werden kann, so wie:

<state key="Question1" page="~/Question1.aspx" title="Question 1">

Und anschließend bereitet die CrumbTrailDataSource diesen Titel auf Datenbindung vor:

<asp:HyperLink ID="Question1" runat="server"
  NavigateUrl='<%# Eval("NavigationLink") %>' 
  Text='<%# Eval("Title") %>'/>

Wenn man dieselben Änderungen auf Thanks.aspx anwenden möchte, tritt das zweite Problem im Zusammenhang mit dem Anwendungscode der Umfrage auf, da sämtliche Codebehinds nun leer sind. All diese Mühe jedoch wird jedoch umsonst sein, wenn für die SurveyController kein Komponententest durchgeführt werden kann.

Komponententests

Da wir nun eine schön strukturierte Umfrage-Anwendung haben—leere Codebehinds und sämtliche UI-Logik im Seiten Markup—ist es Zeit für den Komponententests der SurveyController-Klasse. Für die GetQuestion1, GetQuestion2 und GetSummary Datenabruf-Methoden können sicherlich Komponententests durchgeführt werden, da sie keinen Web-spezifischen Code enthalten. Nur die Methoden UpdateQuestion1 und Update­Question2 stellen eine Herausforderung für die Komponententests dar. Ohne das Navigation Framework würden diese beiden Methoden Routing- und Umleitungsaufrufe enthalten—was die traditionelle Art der Datenbewegung zwischen ASPX-Seiten ist—und beide würden Ausnahmefehler ausgeben, wenn sie außerhalb einer Web-Umgebung genutzt würden, was die Komponententests bei der ersten Hürde zum Einstürzen bringen würde. Mit dem Navigation Framework jedoch können für diese beiden Methoden vollständig Komponententests durchgeführt werden, ohne dass irgendeine Codeveränderung oder "Mock"-Objekte notwendig wären.

Zu Anfang erzeuge ich ein Komponententestprojekt für die Umfrage. Ein Rechtsklick auf irgendeine Methode in der SurveyController-Klasse und die Auswahl des Menüs "Testprojekt erzeugen..." erzeugt ein Projekt mit den darin eingeschlossenen notwendigen Referenzen und einer Survey­ControllerTest-Klasse.

Sie erinnern sich, dass das Navigation Framework eine Liste mit Angabe der Zustände und Übergänge benötigt, die in der Datei StateInfo.config konfiguriert werden. Damit das Komponententestprojekt dieselbe Navigationskonfiguration verwendet, muss die Datei StateInfo.config aus dem Web-Projekt implementiert sein, wenn die Testeinheiten ausgeführt werden. Mit diesen Anforderungen im Hinterkopf öffne ich das Objekt Local.testsettings solution und setzte das Häkchen für “Enable deployment”. Anschließend versehe ich die SurveyControllerTest-Klasse mit dem DeploymentItem-Attribut, das sich auf die StateInfo.config-Datei bezieht:

[TestClass]
[DeploymentItem(@"Survey\StateInfo.config")]
public class SurveyControllerTest
{
}

Als nächstes muss eine app.config-Datei zum Testprojekt hinzugefügt werden, die auf die implementierte StateInfo.config-Datei zeigt (diese Konfiguration ist ebenfalls im Web Projekt erforderlich, sie wurde jedoch automatisch von der NuGet Installation hinzugefügt):

<configuration>
  <configSections>
    <sectionGroup name="Navigation">
      <section name="StateInfo" type=
        "Navigation.StateInfoSectionHandler, Navigation" />
    </sectionGroup>
  </configSections>
  <Navigation>
    <StateInfo configSource="StateInfo.config" />
  </Navigation>
</configuration>

Mit dieser Konfiguration können die Komponententests nun beginnen. Beim Strukturieren eines Komponententests gehe ich nach dem AAA-Muster vor:

  1. Anordnen: Aufstellen der Vorbedingungen und Testdaten.
  2. Durchführen: Die Komponenten unter Testbedingungen ausführen.
  3. Bewerten Das Ergebnis überprüfen.

Da ich mit der UpdateQuestion1-Methode anfange, zeige ich, was mit jedem dieser drei Schritte erforderlich ist wenn es zum Testen der Navigation und der Datenübergabe in das Navigation Framework kommt.

Im Schritt "Anordnen" wird die Testkomponente erstellt, weiterhin werden das Objekt unter Testbedingungen und die Parameter, die unter Testbedingungen an die Methode übergeben werden müssen, erzeugt. Im Falle von UpdateQuestion1 bedeutet dies das Erstellen eines SurveyController sowie einer Frage, die mit der relevanten Antwort aktualisiert wurde. Hier ist jedoch eine zusätzliche Navigationsbedingung für das Setup erforderlich, die die entstehende Navigation spiegelt, sobald die Webanwendung gestartet wurde. Sobald die Webanwendung der Umfrage gestartet wurde, fängt das Navigation Framework die Anfrage für die Startseite Question1.aspx ab und navigiert zum Dialog, dessen Pfadattribut mit dieser Anforderung in der StateInfo.config-Datei übereinstimmt:

<dialog key="Survey" initial="Question1" path="~/Question1.aspx">

Die Navigation unter Verwendung eines Dialogschlüssels geht auf den Status, der im initialen Attributs erwähnt war, zurück, also ist der Zustand von Question1 erreicht. Da es nicht möglich ist, eine Startseite in einem Komponententest zu laden, muss diese Dialognavigation manuell durchgeführt werden: Diese Sonderbedingung ist erforderlich im Schritt "Anordnen":

StateController.Navigate("Survey");

Der Schritt "Durchführen" ruft die Methode unter Testbedingungen auf. In diesem Schritt wird nur die Frage einschließlich der aktualisierten Antwort an UpdateQuestion1 übergeben, sodass keine navigationsspezifischen Details erforderlich sind.

Im Schritt "Prüfen" werden die Ergebnisse mit den erwarteten Werten überprüft. Die Überprüfung des Ergebnisses von Navigation und Datenübergabe kann durch die Verwendung von Klassen im Navigation Framework geschehen. Sie werden sich daran erinnern, dass der StateContext Zugang zu den statischen Daten über seine Dateneigenschaften herstellt, die mit den NavigationData, die während der Navigation übergeben wurden, initialisiert werden. Damit kann überprüft werden, ob UpdateQuestion1 die ausgewählte Antwort dem nächsten Status übergibt. Wenn man also davon ausgeht, dass “Webformulare” der Methode übergeben werden, wird aus der Überprüfung:

Assert.AreEqual("Web Forms", (string) StateContext.Data["technology"]);

Der StateContext verfügt ebenfalls über eine Status-Eigenschaft, die den aktuellen Status aufzeichnet. Damit kann überprüft werden, ob die Navigation so durchgeführt wurde wie erwartet—dass z. B. die Übergabe von “Web Forms” zu UpdateQuestion1 führt:

Assert.AreEqual("Question2", StateContext.State.Key);

Während der StateContext Details über den aktuellen Status und assoziierte Daten enthält, ist der Crumb die äquivalente Klasse für zuvor besuchte Zustände und ihre Daten-- der Name kommt daher, dass bei jeder Navigationsaktivität eines Nutzers ein neues Datum zum dem "Brotkrumen" (breadcrumb)-Pfad hinzugefügt wird. Dieser "Brotkrumenpfad" bzw. "List of crumbs" ist über die Crumbs-Eigenschaft des StateController zugänglich (und es handelt sich dabei weiterhin um die dahinterliegenden Daten der CrumbTrailDataSource des vorherigen Abschnitts). Ich muss diese Liste wieder herstellen und herauszufinden, dass UpdateQuestion1 die übergebene Antwort in seinen statischen Daten vor der Navigation speichert, denn wenn es erst einmal zur Navigation kommt, wird ein "Krümel" erzeugt, der diese statischen Daten transportieren kann. Gehen wir von der Antwort aus, die an die "Webformulare" übergeben wurde, können die Daten des ersten sowie des letzten "Krümels" überprüft werden.

Assert.AreEqual("Web Forms", (string) StateController.Crumbs[0].Data["answer"]);

Das AAA-Muster, mit dem man einen strukturierten Komponententest durchführen kann, wurde in Bezug auf das Navigation Framework abgedeckt. Führt man alle diese Schritte zusammen, geht es in einem Komponententest darum zu überprüfen, ob der Question2-Status erreicht wurde, nachdem eine Antwort der "Webformulare" zu UpdateQuestion1 übergeben wurde:

[TestMethod]
public void UpdateQuestion1NavigatesToQuestion2IfAnswerIsWebForms()
{
  StateController.Navigate("Survey");
  SurveyController controller = new SurveyController();
  Question question = new Question() { Answer = "Web Forms" };
  controller.UpdateQuestion1(question);
  Assert.AreEqual("Question2", StateContext.State.Key);
}

Obwohl dies alles ist, das Sie für das Teste der unterschiedlichen Navigation Framework-Konzepte benötigen, lohnt es sich mit UpdateQuest2 fortzufahren, da dies eine Reihe von Unterschieden in seinen Schritten Anordnen und Ausführen kennt. Die im Schritt Anordnen erforderliche Navigationsbedingung ist anders, da der aktuelle Zustand UpdateQuestion2 sein sollte und die aktuellen Statusdaten die Antwort im "Webformular" enthalten sollten. In der Web-Anwendung werden diese Navigation und Datenübergabe durch das UI verwaltet, da der Nutzer nicht zu der zweiten Frage gelangen kann, ohne zuvor die "Webformulare" der erste beantwortet zu haben. In dieser Komponententestumgebung jedoch muss dies automatisch geschehen. Um den Status von Question1 zu erreichen, ist die gleiche Dialognavigation erforderlich, wie sie von UpdateQuestion1 erfordert wird; daran schließt sich eine Navigation an, die den Übergangsschlüssel Next und die Antwort des Webformulars zu NavigationData übergibt:

StateController.Navigate("Survey");
StateController.Navigate(
  "Next", new NavigationData() { { "technology", "Web Forms" } });

Im Schritt "Überprüfen" kommt der einzige Unterschied für UpdateQuestion2 dann zum Tragen, wenn überprüft wird, dass seine Antwort in Statusdaten gespeichert sind, die der Navigation vorausgehen. Als dieser Check für UpdateQuestion1 durchgeführt wurde, wurde der erste Crumb in dieser Liste verwendet, da nur ein Status besucht wurde, Question1. Für UpdateQuestion2 jedoch wird des zwei "Crumbs" in der Liste geben, da sowohl Question1 also auch Question2 erreicht wurden. Crumbs tauchen in der Liste in der Reihenfolge auf, in der sie aufgesucht wurden, also ist Question2 der zweite Eintrag und der Requisitencheck verläuft wie folgt:

Assert.AreEqual("Yes", (string)StateController.Crumbs[1].Data["answer"]);

Der große Ehrgeiz komponentenbasierter Webformulare ist erreicht. Die Aufgabe wurde erledigt durch die Verwendung von Datenbindung mit der Assistenz vom Navigation Framework. Es ist weniger präskriptiv als andere Komponententests für ASP.NET, da der Kontroller keine Frameworkklasse oder ein Interface erben oder implementieren muss, und seine Methoden mussten bislang noch keine besonderen Framework Typen zurückgeben.

Ist MVC bereits eifersüchtig?

MVC sollte in mancher Hinsicht ein wenig eifersüchtig sein, da die Anwendung der Umfrage genauso gut strukturiert ist wie eine typische MVC-Anwendung, aber auf einer höheren Ebene der Komponententests läuft. Der Navigationscode der Umfrage-Anwendung erscheint innerhalb der Kontroller-Methoden und wird zusammen mit der übrigen Geschäftslogik getestet. In einer MVC-Anwendung wird der Navigationscode nicht getestet, da er innerhalb der Rückgabetypen der Kontrollermethoden enthalten, so wie RedirectResult. In meinem nächsten Artikel über das Navigation Framework werde ich MVCs Eifersucht heraufbeschwören, indem ich eine Suchmaschinenoptimierungs-Freundliche Anwendung mit einer Seite entwerfe, die sich nach den DRY-Prinzipien (Don't repeat yourself) richtet, die in dem entsprechenden MVC-Äquivalent schwer zu erreichen sind.

Das heißt, Datenbindung für Webformulare hat sehr wohl Probleme, die in ihrem MVC-Gegenstück nicht existieren. Es ist z. B. schwierig, eine Abhängigkeitsinjektion (DI) für die Kontrollerklasse zu verwenden, außerdem werden geschachtelte Typen für ViewModel-Klassen nicht unterstützt. Dennoch haben Webformulare viel von MVC gelernt, und die nächste Version von Visual Studio wird mit einer stark verbesserten Datenbindungs-Erfahrung für Webformulare aufwarten können.

Es gibt noch weit mehr Aspekte bezüglich der Anbindung des Navigation Framework an Datenbindung als hier gezeigt werden konnten. Es gibt z. B. eine Daten-Pager-Kontrolle, die—im Unterschied zum ASP.NET DataPager—sich nicht mit einer Kontrolle verbinden brauchen oder eine getrennte Zählmethode erfordern. Wenn Sie mehr darüber erfahren wollen, finden Sie hier eine verständliche Dokumentation und Beispielcode..

Graham Mendick ist einer der größten Fans von Web Forms und möchte zeigen, dass die Architektur von Web Forms genauso solide ist wie die von ASP.NET MVC. Er hat das Navigation for ASP.NET Web Forms-Framework erstellt und ist überzeugt, dass es Web Forms bei Verwendung mit Datenbindung neues Leben einhauchen kann.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Scott Hanselman