it-swarm.com.de

Wie weit sollte ich mit Abhängigkeitsinjektion und Verspottung gehen?

Angenommen, ich habe eine folgende Klasse:

public class A {

  public void execute() {
    if (something) 
      ThirdPartyApi.method();   
  }
}

Nun möchte ich insbesondere die Methode execute() testen. Ich möchte sicherstellen, dass alle Wege abgedeckt sind und wie erwartet funktionieren. Das Problem, auf das ich hier stoßen würde, ist jedoch, dass ich das ThirdPartyApi nicht verspotten kann, da es nicht als Abhängigkeit bereitgestellt wird. Eine einfache Lösung hierfür besteht darin, ThirdPartyApi als solche Abhängigkeit bereitzustellen.

public class A {

  private ThirdPartyApi api;

  public A(ThirdPartyApi api) {
    this.api = api;
  }

  public void execute() {
    if (something) 
      api.method();   
  }
}

Jetzt würde ich die Funktionalität bekommen, die ich will und jeder würde glücklich sein. Was mir hier jedoch nicht gefällt, ist die Tatsache, dass ich alle internen Abhängigkeiten über Konstruktor-/Set-Methoden bereitstellen muss, um meine Klasse vollständig testbar und unabhängig von externen Abhängigkeiten (Android-API, Bibliotheken, Dienstprogramme usw.) zu machen. .

Dies wird zu einem Problem für Abhängigkeiten, die nicht direkt zum Ausführen des Anwendungsfalls der Klasse benötigt werden. Sie sind ein bloßes Werkzeug, beispielsweise ein Einheitenumrechnungswerkzeug. Auf diese Weise fordere ich den Benutzer dieser Klasse auf, mir eine Abhängigkeit bereitzustellen, die ich intern selbst erstellen kann. Dies erschwert die Verwendung der API, da der Benutzer mehr über die Klasse wissen muss, als er sollte/muss.

Um das Beispiel genauer zu machen, sagen wir, dass ThidPartyApi eine riesige externe Einheitenkonvertierungsbibliothek ist. Warum sollte ein Benutzer mir über einen Konstruktor eine Klasse aus dieser Bibliothek bereitstellen müssen? Es ist etwas, das nur ein Nebenwerkzeug ist, um den Anwendungsfall zu erreichen. Es hängt nicht direkt mit dem Anwendungsfall selbst zusammen. Das Erfordernis dieser Abhängigkeit in einem Konstruktor würde die API verwirrender machen. Siehe folgende Klasse:

public class Storage {

  private ConversionTool api;

  public Storage (ConversionTool api) {
    this.api = api;
  }

  public void storeInteger(int a) {
    storeString(api.convertToString(a));
  }
}

vs.

public class Storage {

  private ConversionTool api;

  public Storage () {
    this.api = new ConversionTool();
  }

  public void storeInteger(int a) {
    storeString(api.convertToString(a));
  }
}

Was wäre der bevorzugte Ansatz? Das Bereitstellen von Seitenabhängigkeiten über den Konstruktor macht ihn testbarer und verspottbarer, aber die API verliert an Bedeutung. Wenn keine Seitenabhängigkeiten über den Konstruktor bereitgestellt werden, bleibt die Bedeutung für eine Klasse erhalten, aber ich verliere die Fähigkeit, alles innerhalb der Klasse zu verspotten und die Klasse vollständig zu testen.

7
aarnaut

Warum sollte ein Benutzer mir über einen Konstruktor eine Klasse aus dieser Bibliothek bereitstellen müssen? Es ist etwas, das nur ein Nebenwerkzeug ist, um den Anwendungsfall zu erreichen.

IMHO ist dies nicht die wirklich wichtige Frage. Ob ThirdPartyApi ein "Nebenwerkzeug" ist, spielt keine Rolle. Stattdessen würde ich empfehlen zu fragen

  • verspottet nichtThirdPartyApikönnen Sie trotzdem einfache, stabile und schnelle Tests schreiben?

Wenn die Antwort "Ja" lautet, muss ThirdPartyApi nicht in Ihre Klasse eingefügt werden (zumindest nicht zum Testen). Wenn die Antwort "Nein" lautet, ist DI der bessere Ansatz.

Ihr Beispiel für eine "Konvertierungsbibliothek" sieht so aus, als ob die Antwort "Ja" lauten könnte, so wie Sie normalerweise keine eingebauten Konvertierungsfunktionen Ihrer Programmiersprache oder Standardbibliothek verspotten würden. Wenn für die Erstellung eines ThirdPartyApi -Objekts jedoch eine Datenbankverbindung über das Netzwerk erforderlich ist, da für mehrere Konvertierungsfunktionen Daten aus einer Datenbank erforderlich sind, ist das Einfügen möglicherweise der bessere Ansatz.

Beachten Sie auch, dass es andere Gründe geben kann, ThirdPartyApi injizierbar zu machen als nur Tests. Zum Beispiel, weil zu viele herstellerspezifische Kopplungen vermieden werden oder weil Sie unterschiedliche "Conversion-Strategien" haben möchten.

DI ist kein Selbstzweck, sondern Mittel und Zweck. Verwenden Sie es, wenn Sie es brauchen, nicht "nur für den Fall".

6
Doc Brown

Wie weit sollte ich mit Abhängigkeitsinjektion und Verspottung gehen?

Die absichtlich vage Antwort lautet "so weit Sie müssen, um ein gutes Design zu produzieren".

1972, Parnas beschrieb ein interessantes Prinzip für das Moduldesign

zunächst beginnt eine Liste schwieriger Entwurfsentscheidungen oder Entwurfsentscheidungen, die sich wahrscheinlich ändern werden. Jedes Modul soll dann eine solche Entscheidung vor den anderen verbergen.

Eine Implementierung von Drittanbietern, auf die wir beim Testen verzichten möchten, ist ein ziemlich gutes Beispiel für eine Entscheidung, die sich "wahrscheinlich ändern wird".

Die Einführung der Fähigkeit, eine bestimmte Implementierung bereitzustellen, bedeutet nicht , dass die API-Vorteile für Kompositionswurzel zusätzliche Komplexität aufweisen müssen

public Storage () {
    this(new ConversionTool());
}

Storage (ConversionTool api) {
    this.api = api;
}

Diese Art von Ansatz führt nicht getestete Codepfade ein, aber der nicht getestete Pfad erfüllt diese wichtige Einschränkung: Sie sind "so einfach, dass offensichtlich keine Mängel vorliegen".

Die verwandte Frage lautet: "Wollen wir diese Drittanbieter-Bibliothek während des Testens entfernen?" Es gibt Eigenschaften, die unsere Tests haben sollen - wir möchten sie häufig, daher schnell, also parallel und ohne langsame Nebenwirkungen ausführen können. Wenn die Bibliothek eines Drittanbieters diese Eigenschaften teuer oder unmöglich macht, möchten wir die Bibliothek natürlich durch ein Testdouble ersetzen können.

Wenn die Bibliothek eines Drittanbieters testbar ist, möchten wir sie möglicherweise trotzdem ersetzen. Rainsberger macht dies in seinem Vortrag geltend integrierte Tests sind ein Betrug . TL; DR - Sie können Ihr Design effizienter testen, wenn Sie die Codepfadvariationen im Code von Drittanbietern nicht berücksichtigen müssen.

4
VoiceOfUnreason

Ich weiß, dass ich zu spät zu dieser Party komme, aber ich habe etwas zu sagen:

Verwenden Sie TDD nicht mehr als einzige Überlegung in Ihrem Design!

Es war nie dafür gedacht. Es ist ein schreckliches Missverständnis dessen, was die TDD-Befürworter sagten, als sie argumentierten, dass eine Neugestaltung, um Ihren Code testbar zu machen, das Design verbessert. Es tut. Wenn Sie es nicht vermasseln.

Ja, es ist möglich, ansonsten einfach zu erstellende Objektgraphen in einen Albtraum zu verwandeln. Aber das ist nicht TDDs Schuld. Es gibt einige einfache Prinzipien, die diese Komplexität mindern.

Konvention über Konfiguration ist eine sehr einfache Lösung für dieses Problem. Grundsätzlich heißt es, dass es in Ordnung ist, Standardwerte fest zu codieren, solange sie überschrieben werden können. Dieser Rat ist in Sprachen, in denen Parameter und damit Standardargumente benannt sind, sehr einfach zu befolgen. Wenn Sie wie in Java ohne sie stecken bleiben, müssen Sie sie am Ende mit dem Builder-Muster von Joshua Blochs simulieren, wenn es für Sie wichtig ist, unveränderlich zu sein. Wenn veränderlich in Ordnung ist, lösen Setter dies leicht. Wenn Sie sich in C # oder Python oder so) befinden, haben Sie bereits Parameter benannt, also hören Sie auf, sich zu entschuldigen.

Eine andere Lösung ist die abstrakte Konstruktion mit Fabriken. Alles direkt in der Hauptleitung zu erledigen, kann schnell unhandlich werden. Solange Sie sich einen guten Namen dafür vorstellen können, können Sie eine Fabrik mit fest codierten Werten verpacken, die die Konstruktion vereinfachen, ohne zu diktieren, dass nur so gebaut werden kann. Ich mag diese Methode, weil lange Konstruktoren viel weniger schmerzhaft sind, wenn Sie sie nie berühren müssen.

Wenn Sie denken, dass dies zu viel Arbeit nur zum Testen ist, haben Sie absolut Recht. Ich mache das nicht zum Testen. Ich mache das, weil ich mein Recht, meine Drittanbieter-API zu ändern, nicht behalten möchte, wenn es mir verdammt gut geht.

Dazu muss ich sicher sein, dass ich nicht nur Tests die Bibliothek verspotten lasse. Ich muss nur sorgfältig ausdrücken, was meine Domain-Objekte wirklich an sie übergeben müssen. Nichts mehr. ThirdPartyLibrary hat vielleicht 35 nützliche Methoden, aber wenn ich nur zwei brauche, werde ich klarstellen, dass ich nur diese beiden brauche.

Warum? Denn wenn später ThirdPartyLibrary plötzlich seine Lizenzvereinbarung für mich ändern möchte, werde ich nicht als Geisel gehalten. Ich werde zwei dumme Methoden schreiben und mit meinem Leben weitermachen.

Wenn Sie zulassen, dass die Bibliotheken Ihren Sprachraum so stark infiltrieren, dass Sie nicht für einen Job in Ihrer Codebasis werben können, ohne die Bibliothek zu erwähnen. Denken Sie daran, wenn Sie entscheiden, wie Sie es verwenden möchten.

3
candied_orange

Eine gute Faustregel ist, dass man Spott vermeiden sollte reine Funktionen, dh. diejenigen, deren Ausgänge ausschließlich von den Eingängen abhängen. Es ist nicht erforderlich, eine Einheitenkonvertierungsbibliothek zu verspotten, aus dem gleichen Grund, aus dem wir die Standard-Mathematikbibliothek oder die Routinen für die Zeichenfolgenverarbeitung nicht verspotten.

Im Gegensatz dazu gibt es einen sehr guten Grund, Festplatten-E/A, Netzwerkverkehr, Datenbankabfragen usw. zu verspotten: Sie hängen von der Außenwelt ab und haben viele veränderbare Zustände. Es ist nicht garantiert, dass dieselbe Aufrufsequenz zu denselben Ergebnissen führt (ein anderes Programm könnte Ihre Datei überschreiben, der Benutzer könnte den Flugzeugmodus aktivieren usw.)/Es ermöglicht auch das Testen der Robustheit unter seltenen Bedingungen, wie z. B. zeitweiligen Fehlern.

Ein Grund, eine reine Funktion zu verspotten, wäre jedoch die Leistung. Wenn Sie beispielsweise ein Spiel testen, kann es sich lohnen, die Pfadfindung zu verspotten, damit weniger Zeit für die Ausführung des A * -Algorithmus aufgewendet wird, wenn dieser nicht direkt getestet wird.

3
Alex Reinking

Der bevorzugte Ansatz besteht im Allgemeinen darin, eine Schnittstelle für "etwas zum Konvertieren von Inhalten" zu erstellen, diese als Abhängigkeit hinzuzufügen und eine Implementierung bereitzustellen, die die Bibliothek eines Drittanbieters verwendet. Dies ermöglicht Ihnen Flexibilität für den Fall, dass sie nicht mehr existieren, Ihnen zu viel in Rechnung stellen oder auf andere Weise nicht mehr funktionieren.

Oder je nachdem, wo es verwendet wird (Storage hier), können Sie diese Klasse als die Implementierung betrachten, die für Ihre Drittanbieter-Bibliothek spezifisch ist. Aber dann können Sie es nicht testen. Manchmal ist das in Ordnung, weil die Klasse nicht viel tut.

Im Allgemeinen möchten Sie jedoch Dinge so entkoppeln, dass die Drittanbieter-Bibliothek nur eine Klasse berührt, die Sie dann für den Integrationstest benötigen.

0
Telastyn