ASP.NET MVC

Testen von ASP.NET MVC

Keith Burnell

Im Zentrum des Model-View-Controller (MVC)-Musters steht die Aufteilung von UI-Funktionen in drei Komponenten. Das Modell repräsentiert die Daten und das Verhalten Ihrer Domäne. Die Ansicht verwaltet die Anzeige des Modells und verarbeitet die Interaktion mit den Benutzern. Der Controller orchestriert die Interaktionen zwischen der Ansicht und dem Modell. Die Trennung der inhärent schwer zu testenden UI-Logik von der Geschäftslogik macht Anwendungen, bei denen das MVC-Muster implementiert ist, außerordentlich einfach zu testen. In diesem Artikel möchte ich bewährte Verfahren und Techniken zur Verbesserung der Testbarkeit von ASP.NET MVC-Anwendungen besprechen, einschließlich der Strukturierung der Lösung, der Gestaltung des Codes zur Behandlung der Injektion von Abhängigkeiten und der Implementierung der Abhängigkeitsinjektion mit StructureMap.

Strukturieren der Lösung für maximale Testbarkeit

Der beste Ausgangspunkt für unsere Diskussion liegt da, wo jeder Entwickler ein neues Projekt beginnt: beim Erstellen der Lösung. Ich diskutiere einige bewährte Verfahren zum Entwerfen Ihrer Visual Studio-Lösung auf Grundlage meiner eigenen Erfahrungen mit dem Entwickeln großer ASP.NET MVC-Unternehmensanwendungen unter Verwendung der testgesteuerten Entwicklung (Test-Driven Development, TDD). Für den Anfang schlage ich vor, die leere Projektvorlage zu verwenden, wenn Sie ein ASP.NET MVC-Projekt erstellen. Die anderen Vorlagen sind gut geeignet zum Experimentieren oder zum Erstellen von Machbarkeitsstudien, sie enthalten jedoch meist viel ablenkendes Rauschen, das in einer wirklichen Unternehmensanwendung unnötig ist.

Wenn Sie eine komplexe Anwendung erstellen, sollten Sie ein n-Schichtverfahren verwenden. Für die Entwicklung von ASP.NET MVC-Anwendungen schlage ich das in Abbildung 1 und Abbildung 2 dargestellte Verfahren vor, das die folgenden Projekte enthält:

  • Das Webprojekt enthält den gesamten UI-spezifischen Code, einschließlich Ansichten, Ansichtsmodellen, Skripten, CSS und so weiter. Diese Schicht kann nur auf Controller-, Service-, Domänen- und freigegebene Projekte zugreifen.
  • Das Controller-Projekt enthält die Controllerklassen, die von ASP.NET MVC verwendet werden. Diese Schicht kommuniziert mit den Dienst-, Domänen- und freigegebenen Projekten.
  • Das Dienstprojekt enthält die Geschäftslogik der Anwendung. Diese Schicht kommuniziert mit den DataAccess-, Domänen- und freigegebenen Projekten.
  • Das DataAccess-Projekt enthält den Code, der verwendet wird, um die Daten, die die Anwendung festlegen, abzurufen und zu bearbeiten. Diese Schicht kommuniziert mit den Domänen- und freigegebenen Projekten.
  • Das Domänenprojekt enthält die Domänenobjekte, die von der Anwendung verwendet werden, und es darf mit keinem der Projekte kommunizieren.
  • Das freigegebene Projekt enthält Codes, die für mehrere andere Schichten verfügbar sein müssen, wie Protokollierungen, Konstanten und weitere gemeinsamen Dienstprogrammcodes. Sie darf nur mit dem Domänenprojekt kommunizieren.

Interaction Among Layers
Abbildung 1 Interaktion zwischen Schichten

Example Solution Structure
Abbildung 2. Beispiellösungsstruktur

Ich empfehle, die Controller in ein separates Visual Studio-Projekt zu platzieren. Informationen dazu, wie sich dies einfach erreichen lässt, finden sich unter bit.ly/K4mF2B. Indem Sie die Controller in ein separates Projekt platzieren, können Sie die Logik, die in den Controllern enthalten ist, weiter vom UI-Code entkoppeln. Das Ergebnis ist, dass Ihr Webprojekt nur Code enthält, der sich wirklich auf die UI bezieht.

Wo sollen die Testprojekte platziert werden? Es ist wichtig, wo sie die Testprojekte platzieren und wie Sie sie benennen. Wenn Sie komplexe Anwendungen auf Unternehmensebene entwickeln, werden die Lösungen tendenziell sehr groß, sodass es schwierig werden kann, eine spezifische Codeklasse oder einen Codeabschnitt im Projektmappen-Explorer zu finden. Wenn Sie der vorhandenen Codebasis mehrere Testprojekte hinzufügen, wird die Navigation im Projektmappen-Explorer nur noch komplizierter. Ich empfehle Ihnen nachdrücklich, die Testprojekte von dem eigentlichen Anwendungscode physisch zu trennen. Ich schlage vor, alle Testprojekte in einem Testordner auf der Lösungsebene zu platzieren. Wenn Sie alle Testprojekte und Tests in einem einzigen Lösungsordner unterbringen, erreichen eine wesentliche Reduzierung des Rauschens in Ihrer Projektmappen-Explorer-Standardansicht, und Sie können Ihre Tests leicht finden.

Als Nächstes möchten Sie die Testtypen trennen. Höchstwahrscheinlich enthält Ihre Lösung eine Vielzahl von Testtypen (Einheit, Integration, Leistung, UI usw.), und es ist wichtig, jeden Testtyp zu isolieren und zu gruppieren. Dies erleichtert nicht nur das Auffinden spezifischer Testtypen, sondern es erlaubt Ihnen auch, alle Tests eines spezifischen Typs einfach auszuführen. Wenn Sie eine der beliebtesten Visual Studio-Produktivitätstoolsuiten, ReSharper (jetbrains.com/ReSharper) oder CodeRush (devexpress.com/CodeRush) verwenden, erhalten Sie ein Kontextmenü, das es Ihnen erlaubt, mit der rechten Maustaste auf einen beliebigen Ordner, ein Projekt oder eine Klasse im Projektmappen-Explorer zu klicken und alle Tests, die in dem Element enthalten sind, auszuführen. Um Tests nach Testtyp zu gruppieren, erstellen Sie einen Ordner für jeden Testtyp, den Sie innerhalb des Testlösungsordners zu schreiben planen.

Abbildung 3 zeigt ein Beispiel eines Testlösungsordners mit einer Anzahl von Testtypenordnern.

An Example Tests Solution Folder
Abbildung 3 Ein Beispiel für einen Testlösungsordner

Benennen der Testprojekte Wie Sie die Testprojekte benennen, ist genau so wichtig wie der Ort, wo Sie sie platzieren. Sie möchten in der Lage sein, schnell und leicht zu erkennen, welcher Teil Ihrer Anwendung gerade in den einzelnen Testprojekten getestet wird und welche Testtypen das Projekt enthält. Daher ist es eine gute Idee, die Testprojekte unter Verwendung der folgenden Konvention zu benennen: [Voller Name des zu testenden Projekts].Test.[Testtyp]. Dies erlaubt Ihnen, auf einen Blick zu genau zu erkennen, welche Schicht Ihres Projekts gerade getestet wird und welcher Testtyp durchgeführt wird. Vielleicht meinen Sie, es sei überflüssig, die Testprojekte in typspezifische Ordner zu platzieren und den Testtyp im Namen des Testprojekts hinzuzufügen, erinnern Sie sich jedoch daran, dass die Lösungsordner nur im Projektmappen-Explorer verwendet werden und nicht im Namespace der Projektdatei enthalten sind. Obwohl sich also das Controller-Komponententestprojekt im Tests\Komponentenlösungsordner befindet, gibt der Namespace – TestDrivingMVC.Controllers.Test.Unit – nicht diese Ordnerstruktur wieder. Das Hinzufügen des Testtyps beim Benennen des Projekts ist notwendig, um Benennungskonflikte zu vermeiden und festzustellen, welche Art von Test Sie im Editor bearbeiten. Abbildung 4 zeigt den Projektmappen-Explorer mit Testprojekten.

Test Projects in Solution Explorer
Abbildung 4 Testprojekte im Projektmappen-Explorer

Einführen von Abhängigkeitsinjektion in die Architektur

Sie können mit den Komponententests einer n-Schichtanwendung nicht sehr weit kommen, ohne eine Abhängigkeit in dem zu testenden Code zu entdecken. Bei diesen Abhängigkeiten kann es sich um weitere Schichten der Anwendung handeln, oder sie können sich vollkommen außerhalb Ihres Codes befinden (etwa eine Datenbank, ein Dateisystem oder Webdienste). Wenn Sie Komponententests schreiben, müssen Sie mit dieser Situation korrekt umgehen und Testdoubles (Mocks, Fakes oder Stubs) verwenden, wenn Sie auf eine externe Abhängigkeit stoßen. Weitere Informationen zu Testdoubles finden Sie in „Exploring the Continuum of Test Doubles“ (msdn.microsoft.com/magazine/cc163358) in der September 2007-Ausgabe des MSDN-Magazins. Bevor Sie die von den Testdoubles bereitgestellte Flexibilität nutzen können, muss Ihr Code jedoch so strukturiert werden, dass er Abhängigkeitsinjektionen behandeln kann.

Abhängigkeitsinjektion Abhängigkeitsinjektion ist der Vorgang der Injizierung der konkreten Implementierungen, die eine Klasse erfordert, anstelle eines direkten Instanziieren der Abhängigkeit durch die Klasse. Die verarbeitende Klasse ist sich keiner tatsächlichen konkreten Implementierung irgendeiner ihrer Abhängigkeiten bewusst, sondern kennt nur die Schnittstellen, die die Abhängigkeiten unterstützen; die konkreten Implementierungen werden entweder von der verarbeitenden Klasse oder einem Abhängigkeitsinjektionsframework bereitgestellt.

Das Ziel der Abhängigkeitsinjektion besteht darin, einen extrem lose gekoppelten Code zu erstellen. Die lose Kopplung erlaubt Ihnen, Testdoubleimplementierungen Ihrer Abhängigkeiten leicht zu ersetzen, wenn Sie Komponententests schreiben.

Eine Abhängigkeitsinjektion kann auf drei verschiedene Weisen durchgeführt werden:

  • Eigenschaftsinjektion
  • Konstruktorinjektion
  • Verwenden eines Abhängigkeitsinjektionsframework-/Inversion of Control-Containers (im Folgenden DI/IoC-Framework genannt)

Mit der Eigenschaftsinjektion machen Sie öffentliche Eigenschaften an Ihrem Objekt verfügbar, damit seine Abhängigkeit festgelegt werden können, wie in Abbildung 5 gezeigt. Dieser Ansatz ist unkompliziert und erfordert keine Tools.

Abbildung 5 Eigenschaftsinjektion

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {}
  public ILoggingService LoggingService { get; set; }
  public decimal CalculateSalary(long employeeId) {
    EnsureDependenciesSatisfied();
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
  private void EnsureDependenciesSatisfied() {
    if (_loggingService == null)
      throw new InvalidOperationException(
        "Logging Service dependency must be satisfied!");
    }
  }
}
// Employee Controller (Consumer of Employee Service)
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long id) {
    EmployeeService employeeService = new EmployeeService();
    employeeService.LoggingService = new LoggingService();
    decimal salary = employeeService.CalculateSalary(id);
    return View(salary);
  }
}

Dieses Verfahren hat drei Nachteile. Erstens verlangt es vom Verbraucher, die Abhängigkeiten bereitzustellen. Zweitens erfordert es, dass Sie Schutzcode in Ihre Objekte implementieren, um sicherzustellen, dass die Abhängigkeiten festgestellt sind, bevor sie verwendet werden. Und schließlich, wenn die Anzahl der Abhängigkeiten, über die Ihr Objekt verfügt, zunimmt, nimmt auch die Menge des Codes zu, der erforderlich ist, um das Objekt zu instanziieren.

Das Implementieren von Abhängigkeitsinjektion mit Konstruktorinjektion umfasst das Bereitstellen von Abhängigkeiten für eine Klasse über deren Konstruktor, wenn der Konstruktor instanziiert wird, wie in Abbildung 6 gezeigt. Dieses Verfahren ist ebenfalls unkompliziert, aber anders als bei der Eigenschaftsinjektion können Sie sich darauf verlassen, dass die Abhängigkeiten der Klasse immer eingestellt sind.

Abbildung 6 Konstruktorinjektion

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService(ILoggingService loggingService) {
    _loggingService = loggingService;
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}
// Consumer of Employee Service
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long employeeId) {
    EmployeeService employeeService =
      new EmployeeService(new LoggingService());
    decimal salary = employeeService.CalculateSalary(employeeId);
    return View(salary);
  }
}

Unglücklicherweise erfordert auch dieses Verfahren, dass der Verbraucher die Abhängigkeiten bereitstellen muss. Außerdem ist es eigentlich nur für kleine Anwendungen geeignet. Größere Anwendungen haben meistens zu viele Abhängigkeiten, um sie über den Konstruktor des Objekts bereitzustellen.

Das dritte Verfahren zum Implementieren einer Abhängigkeitsinjektion besteht in der Verwendung eines DI/IoC-Frameworks. Ein DI/IoC-Framework befreit den Verbraucher völlig von der Verantwortung für die Bereitstellung von Abhängigkeiten und erlaubt Ihnen, Ihre Abhängigkeiten zur Entwurfszeit zu konfigurieren, sodass sie zur Laufzeit gelöst sind. Es sind viele DI/IoC-Frameworks für .NET verfügbar, darunter Unity (das Angebot von Microsoft), StructureMap, Castle Windsor, Ninject und weitere. Allen unterschiedlichen DI/IoC-Frameworks liegt dieselbe Konzeption zugrunde, und die Entscheidung für eine darunter ist normalerweise eine Frage der persönlichen Vorliebe. Um in diesem Artikel DI/IoC-Frameworks vorzuführen, verwende ich StructureMap.

Die nächste Stufe der Abhängigkeitsinjektion – mit StructureMap

StructureMap (structuremap.net) ist ein weit verbreitetes DI-Framework. Sie können es über NuGet entweder mit der Paket-Manager-Konsole (Install-Package StructureMap) oder mit dem NuGet Package Manager-GUI installieren (klicken Sie mit der rechten Maustaste auf den Verweisordner des Projekts und wählen Sie „NuGet-Pakete verwalten“ aus).

Abhängigkeiten mit StructureMap konfigurieren Der erste Schritt beim Implementieren von StructureMap in ASP.NET MVC besteht darin, die Abhängigkeiten so zu konfigurieren, dass StructureMap sie lösen kann. Dies können Sie in der Application_Start-Methode von Global.asax auf eine der folgenden beiden Arten tun.

Das erste Verfahren besteht darin, StructureMap manuell zu erklären, dass es für eine spezifische abstrakte Implementierung eine spezifische konkrete Implementierung verwenden soll:

ObjectFactory.Initialize(register => {
  register.For<ILoggingService>().Use<LoggingService>();
  register.For<IEmployeeService>().Use<EmployeeService>();
});

Ein Nachteil dieses Verfahrens besteht darin, dass Sie jede Abhängigkeit manuell in Ihrer Anwendung registrieren müssen, und bei großen Anwendungen kann das mühsam werden. Außerdem muss Ihre Webschicht, da Sie die Abhängigkeiten im Application_Start Ihrer ASP.NET MVC-Site registrieren, über eine direkte Kenntnis jeder anderen Schicht Ihrer Anwendung verfügen, in der Abhängigkeiten hinzugefügt werden müssen.

Sie können auch die StructureMap-Selbstregistrierungs- und Scanfunktionen verwenden, um Ihre Assemblys zu überprüfen und Abhängigkeiten automatisch hinzuzufügen. Bei diesem Verfahren überprüft StructureMap Ihre Assemblys, und wenn es auf eine Schnittstelle trifft, sucht es nach einer zugehörigen konkreten Implementierung (basierend auf der Tatsache, dass eine Schnittstelle namens IFoo nach Konvention der konkreten Implementierung Foo zugeordnet wird):

ObjectFactory.Initialize(registry => registry.Scan(x => {
  x.AssembliesFromApplicationBaseDirectory();
  x.WithDefaultConventions();
}));

StructureMap-Abhängigkeitskonfliktlöser Wenn Sie die Abhängigkeiten konfiguriert haben, müssen Sie in der Lage sein, von Ihrer Codebasis aus darauf zuzugreifen. Dies geschieht durch Erstellen eines Abhängigkeitskonfliktlösers und dessen Speicherung im freigegebenen Projekt (da von allen Schichten der Anwendung aus, die über Abhängigkeiten verfügen, darauf zugegriffen werden muss): 

public static class Resolver {
  public static T GetConcreteInstanceOf<T>() {
    return ObjectFactory.GetInstance<T>();
  }
}

Die Konfliktlöserklasse (wie ich sie nenne, da Microsoft eine DependencyResolver-Klasse mit ASP.NET MVC 3 eingeführt hat, auf die ich gleich eingehen werde) ist eine einfache statische Klasse, die eine Funktion enthält. Die Funktion akzeptiert einen generischen Parameter T, der die Schnittstelle repräsentiert, für die Sie eine konkrete Implementierung suchen, und gibt T zurück, das die tatsächliche Implementierung der übergebenen Schnittstelle ist.

Ehe ich darauf eingehe, wie die neue Konfliktlöserklasse in Ihrem Code verwendet wird, möchte ich erwähnen, warum ich meinen eigenen Eigenbau-Abhängigkeitskonfliktlöser geschrieben habe, anstatt eine Klasse zu erstellen, die die IDependencyResolver-Schnittstelle, die mit ASP.NET MVC 3 eingeführt wurde, implementiert. Die Einschließung der IDependencyResolver-Funktionalität ist eine großartige Erweiterung von ASP.NET MVC und ein großer Schritt zur Förderung angemessener Softwareverfahren. Unglücklicherweise befindet sie sich in der System.Web.MVC DLL, und ich möchte keine Verweise auf eine webtechnologiespezifische Bibliothek in den nicht webbezogenen Schichten meiner Anwendungsarchitektur.

Auflösen von Abhängigkeiten im Code Jetzt, nachdem die harte Arbeit getan ist, ist die Auflösung von Abhängigkeiten im Code einfach. Alles, was Sie tun müssen, ist, die GetConcreteInstanceOf-Funktion der Konfliktlöserklasse aufzurufen und ihr die Schnittstelle zu übergeben, für die Sie eine konkrete Implementierung suchen, wie in Abbildung 7 gezeigt.

Abbildung 7 Abhängigkeiten im Code auflösen

public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {
    _loggingService = 
      Resolver.GetConcreteInstanceOf<ILoggingService>();
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}

StructureMap nutzen, um Testdoubles in Komponententests zu injizieren Jetzt, nachdem der Code so eingerichtet, ist, dass Sie ohne Intervention des Verbrauchers Abhängigkeiten injizieren können, wollen wir zur ursprünglichen Aufgabe zurückkehren, nämlich korrekt mit Abhängigkeiten in Komponententests umzugehen. Hier ist das Szenario:

  • Die Aufgabe ist, mit TDD eine Logik zu schreiben, die den Gehaltswert generiert, der von der CalculateSalary-Methode des EmployeeService zurückgegeben wird. (Sie finden die Funktionen EmployeeService und CalculateSalary in Abbildung 7.)
  • Es ist erforderlich, das alle Aufrufe der CalculateSalary-Funktion protokolliert werden.
  • Die Schnittstelle des Protokollierdienstes ist festgelegt, die Implementierung ist jedoch nicht abgeschlossen. Das Aufrufen des Protokollierdienstes löst derzeit einen Ausnahmefehler aus.
  • Die Aufgabe muss abgeschlossen werden, ehe die Arbeit am Protokollierdienst planmäßig startet.

Diese Art von Szenario ist Ihnen mit Sicherheit schon einmal begegnet. Diesmal allerdings verfügen Sie über die geeignete Architektur, um die Verbindung mit der Abhängigkeit zu durchtrennen, indem Sie ein Testdouble an deren Stelle setzen. Ich erstelle meine Testdoubles gerne in einem Projekt, das für alle meine Testprojekte freigegeben werden kann. Wie Sie in Abbildung 8 sehen können, habe ich in meinem Testlösungsordner ein freigegebenes Projekt erstellt. Innerhalb des Projekts habe ich einen Fakes-Ordner hinzugefügt, da ich, um das Testen abzuschließen, eine Fakeimplementierung des ILoggingService benötige.

Project for Shared Test Code and Fakes
Abbildung 8 Projekt für freigegebenen Testcode und Fakes

Das Erstellen eines Fakes für den Protokollierdienst ist einfach. Zuerst erstelle ich in meinem Fakes-Ordner eine Klasse namens LoggingServiceFake. LoggingServiceFake muss dem Vertrag entsprechen, den EmployeeService erwartet, d. h., es muss den ILoggingService und seine Verfahren implementieren. Definitionsgemäß ist ein Fake ein Ersatz, der gerade genug Code enthält, um die Anforderungen der Schnittstelle zu erfüllen. Normalerweise heißt das, dass er über leere Implementierungen von „Void“-Methoden verfügt, und die Funktionsimplementierungen eine Return-Anweisung enthalten, die einen hartcodierten Wert zurückgibt, wie hier:

public class LoggingServiceFake : ILoggingService {
  public void LogError(string message, Exception ex) {}
  public void LogDebug(string message) {}
  public bool IsOnline() {
    return true;
  }
}

Nachdem jetzt der Fake implementiert ist, kann ich meinen Test schreiben. Für den Anfang erstelle ich eine Testklasse im Komponententestprojekt TestDrivingMVC.Service.Test.Unit und, entsprechend den zuvor dargstellten Benennungskonventionen nenne ich es EmployeeServiceTest, wie in Abbildung 9 gezeigt.

Abbildung 9 Die Testklasse EmployeeServiceTest

[TestClass]
public class EmployeeServiceTest {
  private ILoggingService _loggingServiceFake;
  private IEmployeeService _employeeService;
  [TestInitialize]
  public void TestSetup() {
    _loggingServiceFake = new LoggingServiceFake();
    ObjectFactory.Initialize(x => 
      x.For<ILoggingService>().Use(_loggingServiceFake));
    _employeeService = new EmployeeService();
  }
  [TestMethod]
  public void CalculateSalary_ShouldReturn_Decimal() {
    // Arrange
    long employeeId = 12345;
    // Act
    var result = 
      _employeeService.CalculateSalary(employeeId);
    // Assert
    result.ShouldBeType<decimal>();
  }
}

Das Schreiben des Testklassencodes ist größtenteils ziemlich einfach. Die Zeile, auf die Sie besonders achten sollten, ist:

ObjectFactory.Initialize(x =>
    x.For<ILoggingService>().Use(
    _loggingService));

Dies ist der Code, der StructureMap anweist, den LoggingServiceFake zu verwenden, wenn die Konfliktlöserklasse, die wir zuvor erstellt haben, versucht, ILoggingService aufzulösen. Ich habe diesen Code in einer Methode abgelegt, die mit TestInitialize markiert ist, um das Komponententestframework anzuweisen, diese Methode vor dem Ausführen der einzelnen Tests in dieser Testklasse auszuführen.

Mit der Funktion von DI/IoC und dem StructureMap-Tool bin ich in der Lage, die Verbindung zum Protokollierdienst vollständig zu unterbrechen. Dies ermöglicht mir, das Kodieren und Komponententesten abzuschließen, ohne vom Status des Protokollierdienstes abhängig zu sein, und echte Komponententests durchzuführen, die nicht auf irgendwelchen Abhängigkeiten beruhen.

Verwenden von StructureMap als Standard-Controller Factory ASP.NET MVC bietet einen Erweiterbarkeitspunkt, der es Ihnen ermöglicht, eine benutzerdefinierte Implementierung hinzuzufügen, wie Controller in Ihrer Anwendung instanziiert werden. Durch Erstellen einer Klasse, die von DefaultControllerFactory erbt (siehe Abbildung 10) können Sie steuern, wie Controller erstellt werden.

Abbildung 10 Benutzerdefinierte Controller Factory

public class ControllerFactory : DefaultControllerFactory {
  private const string ControllerNotFound = 
  "The controller for path '{0}' could not be found or it does not implement IController.";
  private const string NotAController = "Type requested is not a controller: {0}";
  private const string UnableToResolveController = 
    "Unable to resolve controller: {0}";
  public ControllerFactory() {
    Container = ObjectFactory.Container;
  }
  public IContainer Container { get; set; }
  protected override IController GetControllerInstance(
    RequestContext context, Type controllerType) {
    IController controller;
    if (controllerType == null)
      throw new HttpException(404, String.Format(ControllerNotFound,
      context.HttpContext.Request.Path));
    if (!typeof (IController).IsAssignableFrom(controllerType))
      throw new ArgumentException(string.Format(NotAController,
      controllerType.Name), "controllerType");
    try {
      controller = Container.GetInstance(controllerType) 
        as IController;
    }
    catch (Exception ex) {
      throw new InvalidOperationException(
      String.Format(UnableToResolveController, 
        controllerType.Name), ex);
    }
    return controller;
  }
}

In der neuen Controller Factory verfüge ich über eine öffentliche StructureMap Container-Eigenschaft, die auf Basis der StructureMap ObjectFactory eingerichtet wird (die in Abbildung 10 in der Global.asax konfiguriert wird).

Als Nächstes muss ich die GetControllerInstance-Method außer Kraft setzen, die Typprüfungen durchführt und dann den StructureMap-Container verwendet, um den aktuellen Controller basierend auf dem bereitgestellten Controllertypparameter aufzulösen. Da ich die StructureMap-Selbstregistrierungs- und Scanfunktionen verwendet habe, als ich StructureMap erstmals konfigurierte, muss ich nichts weiter tun.

Der Vorteil beim Erstellen einer benutzerdefinierten Controller Factory besteht darin, dass Sie nicht mehr auf parameterlose Konstruktoren auf Ihren Controllers angewiesen sind. An dieser Stelle fragen Sie sich vielleicht, „Wie würde ich vorgehen, um dem Konstruktor eines Controller Parameter bereitzustellen?“ Dank der Erweiterbarkeit der DefaultControllerFactory und der StructureMap müssen Sie das nicht. Wenn Sie einen parametrisierten Controller für Ihre Controller deklarieren, werden die Abhängigkeiten automatisch aufgelöst, wenn der Controller in der neuen Controller Factory aufgelöst wird.

Wie Sie in Abbildung 11 sehen können, habe ich dem Konstruktor des HomeController einen IEmployeeService-Parameter hinzugefügt. Wenn der Controller in der neuen Controller Factory aufgelöst wird, werden alle vom Konstruktor des Controllers erforderten Parameter automatisch aufgelöst. Das bedeutet, dass Sie den Code zum Auflösen der Abhängigkeiten des Controllers nicht manuell hinzufügen müssen, Sie können jedoch weiterhin Fakes verwenden, wie zuvor dargestellt.

Abbildung 11: Auflösen des Controllers

public class HomeController : Controller {
  private readonly IEmployeeService _employeeService;
  public HomeController(IEmployeeService employeeService) {
    _employeeService = employeeService;
  }
  public ActionResult Index() {
    return View();
  }
  public ActionResult DisplaySalary(long id) {
    decimal salary = _employeeService.CalculateSalary(id);
    return View(salary);
  }
}

Durch Verwendung dieser Verfahren und Techniken in Ihren ASP.NET MVC-Anwendungen positionieren Sie sich für einen einfacheren und saubereren TDD-Prozess.

Keith Burnell ist Senior Software Engineer bei Skyline Technologies. Er entwickelt seit mehr als 10 Jahren Software und ist auf die Entwicklung umfangreicher ASP.NET- und ASP.NET MVC-Websites spezialisiert. Keith Burnell ist ein aktives Mitglied der Entwicklercommunity. Sie finden seinen Blog unter (dotnetdevdude.com) und sein Twitter-Konto unter twitter.com/keburnell.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: John Ptacek und Clark Sell