Gibt es einen echten Wert beim Unit-Testen eines Controllers in ASP.NET MVC?

33

Ich hoffe, diese Frage gibt einige interessante Antworten, weil sie mich eine Weile nervt.

Gibt es einen echten Wert beim Unit-Testen eines Controllers in ASP.NET MVC?

Was ich damit meine, ist, dass meine Controller-Methoden die meiste Zeit (und ich bin kein Genie) selbst in ihrer komplexesten Form so aussehen:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

Das meiste Heben erfolgt entweder über die MVC-Pipeline oder über meine Servicebibliothek.

Vielleicht sollten Sie also folgende Fragen stellen:

  • Was wäre es wert, diese Methode in Einheiten zu testen?
  • Würde es nicht auf Request.UserHostAddressund ModelStatemit einer NullReferenceException brechen? Sollte ich versuchen, diese zu verspotten?
  • Wenn ich diese Methode in einen wiederverwendbaren "Helfer" umwandle (was ich wahrscheinlich tun sollte, wenn ich bedenke, wie oft ich es mache!), lohnt es sich sogar zu testen, wenn alles, was ich wirklich teste, hauptsächlich die "Pipeline" ist, die Wurde es vermutlich von Microsoft auf einen Zentimeter genau getestet?

Ich denke, mein Punkt ist wirklich , das Folgende zu tun, scheint völlig sinnlos und falsch zu sein

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Offensichtlich bin ich mit diesem übertrieben sinnlosen Beispiel stumpf, aber hat jemand Weisheit, die er hier hinzufügen kann?

Freue mich drauf ... Danke.

LiverpoolsNumber9
quelle
Ich denke, der RoI (Return on Investment) für diesen bestimmten Test ist die Mühe nicht wert, es sei denn, Sie haben unendlich viel Zeit und Geld. Ich würde Tests schreiben, auf die Kevin hinweist, um Dinge zu überprüfen, bei denen es wahrscheinlicher ist, dass sie brechen, oder um Ihnen dabei zu helfen, etwas sicher umzugestalten oder sicherzustellen, dass die Fehlerausbreitung wie erwartet erfolgt. Pipeline-Tests können bei Bedarf auf einer globaleren Ebene / Infrastrukturebene durchgeführt werden und auf der Ebene einzelner Methoden sind von geringem Wert. Das heißt nicht, dass sie keinen Wert darstellen, sondern "wenig". Also, wenn es in Ihrem Fall einen guten RoI liefert, versuchen Sie es, sonst fangen Sie zuerst den größeren Fisch!
Mrchief

Antworten:

18

Selbst für etwas so Einfaches wird ein Komponententest mehreren Zwecken dienen

  1. Vertrauen, was geschrieben wurde, entspricht der erwarteten Ausgabe. Es mag trivial erscheinen, zu überprüfen, ob die korrekte Ansicht zurückgegeben wird, aber das Ergebnis ist ein objektiver Beweis dafür, dass die Anforderung erfüllt wurde
  2. Regressionstests. Sollte sich die Create-Methode ändern müssen, haben Sie immer noch einen Komponententest für die erwartete Ausgabe. Ja, die Ausgabe kann sich ändern und dies führt zu einem Sprödigkeitstest, es handelt sich jedoch immer noch um eine Prüfung gegen nicht verwaltete Änderungskontrolle

Für diese spezielle Aktion würde ich Folgendes testen

  1. Was passiert, wenn _myService null ist?
  2. Was passiert, wenn _myService.Create eine Ausnahmebedingung auslöst, werden bestimmte Ausnahmebedingungen behandelt?
  3. Gibt ein erfolgreicher _myService.Create die _Success-Ansicht zurück?
  4. Werden Fehler bis zu ModelState weitergegeben?

Sie haben darauf hingewiesen, dass Sie Request und Model auf NullReferenceException überprüft haben, und ich denke, dass ModelState.IsValid sich um die Behandlung von NullReference for Model kümmert.

Durch das Verspotten der Anforderung können Sie sich gegen eine Nullanforderung schützen, die meiner Meinung nach in der Produktion im Allgemeinen unmöglich ist, aber in einem Komponententest auftreten kann. In einem Integrationstest können Sie unterschiedliche UserHostAddress-Werte angeben. (Eine Anforderung ist immer noch eine Benutzereingabe für das Steuerelement und sollte dementsprechend getestet werden.)

Kevin
quelle
Hallo Kevin, danke, dass du dir die Zeit genommen hast zu antworten. Ich werde eine Weile warten, um zu sehen, ob irgendjemand anders mit irgendetwas hereinkommt, aber bis jetzt ist deins das logischste / klarste.
LiverpoolsNumber9
Spifty. Ich bin froh, dass es dir geholfen hat.
Kevin
3

Meine Controller sind ebenfalls sehr klein. Der größte Teil der "Logik" in Controllern wird mithilfe von Filterattributen (integriert und handgeschrieben) verarbeitet. Daher hat mein Controller normalerweise nur eine Handvoll Jobs:

  • Erstellen Sie Modelle aus HTTP-Abfragezeichenfolgen, Formularwerten usw.
  • Führen Sie eine grundlegende Validierung durch
  • Rufen Sie in meine Daten oder Business-Schicht
  • A. Generieren ActionResult

Die meisten Modellbindungen werden automatisch von ASP.NET MVC durchgeführt. DataAnnotations übernehmen auch für mich den größten Teil der Validierung.

Selbst wenn ich so wenig zu testen habe, schreibe ich sie normalerweise immer noch. Grundsätzlich teste ich, ob meine Repositorys aufgerufen werden und der richtige ActionResultTyp zurückgegeben wird. Ich habe eine bequeme Methode, um ViewResultsicherzustellen, dass der richtige Ansichtspfad zurückgegeben wird und das Ansichtsmodell so aussieht, wie ich es erwartet habe. Ich habe einen anderen, um zu überprüfen, ob der richtige Controller / die richtige Aktion eingestellt ist RedirectToActionResult. Ich habe andere Tests für JsonResultusw. usw.

Eine unglückliche Folge der Unterklassifizierung der ControllerKlasse ist, dass sie viele praktische Methoden bietet, die HttpContextintern verwendet werden. Dies macht es schwierig, die Steuerung einem Komponententest zu unterziehen. Aus diesem Grund setze ich normalerweise HttpContext-abhängige Aufrufe hinter eine Schnittstelle und übergebe diese Schnittstelle an den Konstruktor des Controllers (ich verwende die Ninject-Web-Erweiterung, um meine Controller für mich zu erstellen). Auf dieser Oberfläche werden normalerweise die Eigenschaften des Hilfsprogramms für den Zugriff auf die Sitzung, die Konfigurationseinstellungen, das IP-Prinzip und die URL-Hilfsprogramme festgelegt.

Dies erfordert viel Sorgfalt, aber ich denke, es lohnt sich.

Travis Parks
quelle
Vielen Dank, dass Sie sich die Zeit genommen haben, um gleich zwei Fragen zu beantworten. Erstens sind "Hilfsmethoden" in Komponententests sehr gefährlich. Zweitens: Testen Sie, ob meine Repositorys aufgerufen werden.
LiverpoolsNumber9
Warum sollten Convenience-Methoden gefährlich sein? Ich habe eine BaseControllerTestsKlasse, in der alle leben. Ich verspotte meine Repositories. Ich schließe sie mit Ninject an.
Travis Parks
Was passiert, wenn Sie einen Fehler oder eine falsche Annahme in Ihren Helfern gemacht haben? Mein anderer Punkt war, dass nur ein Integrationstest (dh End-to-End) "testen" kann, ob Ihre Repositorys aufgerufen werden. In einem Unit-Test würden Sie Ihre Repositorys sowieso manuell "neu einrichten" oder verspotten.
LiverpoolsNumber9
Sie übergeben das Repository an den Konstruktor. Sie verspotten es während des Tests. Sie stellen sicher, dass der Schein wie erwartet ausgeführt wird. Die Helfer dekonstruieren ActionResults einfach , um die übergebenen URLs, Modelle usw. zu überprüfen
Travis Parks
Okay, fair genug - ich habe etwas falsch verstanden, was Sie unter "Testen Sie, ob meine Repositorys aufgerufen werden" verstanden haben.
LiverpoolsNumber9
2

Offensichtlich sind einige Controller sehr viel komplexer, basieren aber nur auf Ihrem Beispiel:

Was passiert, wenn myService eine Ausnahme auslöst?

Als Randnotiz.

Außerdem würde ich die Weisheit in Frage stellen, eine Liste als Referenz zu übergeben (dies ist nicht erforderlich, da c # ohnehin als Referenz übergeben wird, auch wenn dies nicht der Fall ist) - eine errorAction-Aktion (Action) übergeben, an die der Dienst dann Fehlermeldungen senden kann Dies könnte dann geschehen, wie Sie möchten (vielleicht möchten Sie es der Liste hinzufügen, vielleicht möchten Sie einen Modellfehler hinzufügen, vielleicht möchten Sie es protokollieren).

In deinem Beispiel:

Anstelle von Referenzfehlern geben Sie beispielsweise (string s) => ModelState.AddModelError ("", s) ein.

Michael
quelle
Erwähnenswert ist, dass sich Ihr Dienst in derselben Anwendung befindet, da sonst Serialisierungsprobleme auftreten.
Michael
Der Dienst würde in einer separaten DLL sein. Aber wie auch immer, Sie haben wahrscheinlich Recht, wenn Sie der "Schiedsrichter" sind. Andererseits spielt es keine Rolle, ob myService eine Ausnahme auslöst. Ich teste myService nicht - ich würde die darin enthaltenen Methoden separat testen. Ich spreche über das reine Testen der ActionResult "Unit" mit (wahrscheinlich) einem verspotteten myService.
LiverpoolsNumber9
Haben Sie eine 1: 1-Zuordnung zwischen Ihrem Dienst und Ihrem Controller? Wenn nicht, verwenden einige Controller mehrere Serviceaufrufe? Wenn ja, könnten Sie diese Interaktionen testen?
Michael
Nein. Am Ende des Tages nehmen die Dienstmethoden Eingaben entgegen (normalerweise ein Ansichtsmodell oder sogar nur Zeichenfolgen / Ints), sie "erledigen Sachen" und geben dann einen Bool / Fehler zurück, wenn sie falsch sind. Es gibt keine "direkte" Verbindung zwischen den Controllern und der Service-Schicht. Die sind komplett getrennt.
LiverpoolsNumber9
Ja, ich verstehe das. Ich versuche, das relationale Modell zwischen den Controllern und der Serviceschicht zu verstehen - vorausgesetzt, dass jeder Controller nicht über eine entsprechende Servicemethode verfügt, ist es naheliegend, dass einige Controller davon Gebrauch machen müssen mehr als eine Service-Methode?
Michael