Allmähliche Ansätze zur Abhängigkeitsinjektion

12

Ich arbeite daran, meine Klassen mit Hilfe von Dependency Injection Unit-testbar zu machen. Aber einige dieser Klassen haben viele Clients, und ich bin noch nicht bereit, alle zu überarbeiten, um die Abhängigkeiten noch zu übergeben. Also versuche ich es allmählich zu tun; Behalten Sie die Standardabhängigkeiten vorerst bei, lassen Sie sie jedoch zum Testen außer Kraft setzen.

Ein Ansatz, den ich verfolge, besteht darin, alle "neuen" Aufrufe in ihre eigenen Methoden zu verschieben, z.

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Dann kann ich in meinen Unit-Tests diese Klasse einfach in Unterklassen unterteilen und die Erstellungsfunktionen überschreiben, sodass sie stattdessen gefälschte Objekte erstellen.

Ist das ein guter Ansatz? Gibt es irgendwelche Nachteile?

Ist es im Allgemeinen in Ordnung, fest codierte Abhängigkeiten zu haben, solange Sie diese zum Testen ersetzen können? Ich weiß, dass der bevorzugte Ansatz darin besteht, sie explizit im Konstruktor zu fordern, und ich möchte irgendwann dorthin gelangen. Aber ich frage mich, ob dies ein guter erster Schritt ist.

Ein Nachteil, der mir gerade aufgefallen ist: Wenn Sie echte Unterklassen haben, die Sie testen müssen, können Sie die Testunterklasse, die Sie für die übergeordnete Klasse geschrieben haben, nicht wiederverwenden. Sie müssen für jede reale Unterklasse eine Test-Unterklasse erstellen und diese muss die Erstellungsfunktionen überschreiben.

JW01
quelle
Können Sie näher erläutern, warum sie jetzt nicht auf Einheiten getestet werden können?
Austin Salonen
Es gibt viele "neue X" im gesamten Code sowie viele Verwendungen von statischen Klassen und Singletons. Wenn ich also versuche, eine Klasse zu testen, teste ich wirklich eine ganze Reihe von ihnen. Wenn ich die Abhängigkeiten isolieren und durch gefälschte Objekte ersetzen kann, habe ich mehr Kontrolle über das Testen.
JW01,

Antworten:

13

Dies ist ein guter Ansatz, um Ihnen den Einstieg zu erleichtern. Beachten Sie, dass es am wichtigsten ist, Ihren vorhandenen Code mit Unit-Tests abzudecken. Sobald Sie die Tests abgeschlossen haben, können Sie die Umgestaltung freier gestalten , um Ihr Design weiter zu verbessern.

Der Ausgangspunkt besteht also nicht darin, das Design elegant und glänzend zu gestalten , sondern die Codeeinheit mit den geringsten Risiken testbar zu machen . Ohne Unit-Tests müssen Sie besonders konservativ und vorsichtig sein, um den Code nicht zu beschädigen. Diese anfänglichen Änderungen können in einigen Fällen sogar dazu führen, dass der Code ungeschickter oder hässlicher aussieht. Wenn Sie jedoch die ersten Komponententests schreiben können, können Sie möglicherweise eine Umgestaltung in Richtung des von Ihnen gewünschten idealen Designs vornehmen.

Die grundlegende Arbeit, die Sie zu diesem Thema lesen sollten, besteht darin, mit Legacy-Code effektiv zu arbeiten . Es beschreibt den Trick, den Sie oben zeigen, und viele, viele weitere in mehreren Sprachen.

Péter Török
quelle
Nur eine Bemerkung dazu: "Sobald Sie die Tests haben, können Sie freier umgestalten." - Wenn Sie keine Tests haben, nicht umgestalten, ändern Sie nur den Code.
ocodo
@ Slomojo Ich verstehe Ihren Standpunkt, aber Sie können immer noch eine Menge sicheres Refactoring ohne Tests durchführen, insbesondere automatisierte IDE-Ractorings, wie zum Beispiel (in Eclipse für Java) das Extrahieren von dupliziertem Code in Methoden, das Umbenennen von allem, das Verschieben von Klassen und das Extrahieren von Feldern, Konstanten etc.
Alb
Ich zitiere hier nur die Ursprünge des Begriffs Refactoring, der darin besteht, Code in verbesserte Muster umzuwandeln, die durch ein Testframework vor Regressionsfehlern geschützt sind. Natürlich hat es IDE-basiertes Refactoring wesentlich einfacher gemacht, Software zu refactern, aber ich neige dazu, Selbsttäuschungen zu vermeiden, wenn meine Software keine Tests enthält, und ich "refactere" nur den Code. Natürlich kann das nicht das sein, was ich jemand anderem erzähle;)
ocodo
1
@Alb, Refactoring ohne Tests ist immer riskanter. Automatische Refactorings verringern dieses Risiko zwar, aber nicht auf Null. Es gibt immer knifflige Fälle, mit denen Werkzeuge möglicherweise nicht richtig umgehen können. Im besseren Fall ist das Ergebnis eine Fehlermeldung des Tools, aber im schlimmsten Fall können im Hintergrund subtile Fehler auftreten. Es wird daher empfohlen, auch mit automatisierten Werkzeugen die einfachsten und sichersten Änderungen vorzunehmen.
Péter Török
3

Die Guice-Leute empfehlen die Verwendung von

@Inject
public void setX(.... X x) {
   this.x = x;
}

für alle Eigenschaften, anstatt nur @Inject zu ihrer Definition hinzuzufügen. Auf diese Weise können Sie sie wie normale Beans behandeln, die Sie newbeim Testen (und beim manuellen Festlegen der Eigenschaften) und bei @Inject in der Produktion verwenden können.


quelle
3

Wenn ich mit dieser Art von Problem konfrontiert werde, werde ich jede Abhängigkeit meiner zu testenden Klasse identifizieren und einen geschützten Konstruktor erstellen, der es mir ermöglicht, sie weiterzugeben Deklarieren von Abhängigkeiten in meinen Konstruktoren, damit ich nicht vergesse, sie in Zukunft zu instanziieren.

Meine neue prüfbare Einheitenklasse wird also 2 Konstruktoren haben:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}
Seth M.
quelle
2

Möglicherweise möchten Sie den Ausdruck "Getter erstellt" in Betracht ziehen, bis Sie die injizierte Abhängigkeit verwenden können.

Beispielsweise,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Auf diese Weise können Sie intern umgestalten, sodass Sie Ihr myObject durch den Getter referenzieren können. Sie müssen nie anrufen new. Wenn in Zukunft alle Clients so konfiguriert sind, dass sie das Einfügen von Abhängigkeiten zulassen, müssen Sie den Erstellungscode nur an einer Stelle entfernen - dem Getter. Mit dem Setter können Sie nach Bedarf Scheinobjekte injizieren.

Der obige Code ist nur ein Beispiel und legt den internen Status direkt offen. Sie sollten sorgfältig prüfen, ob dieser Ansatz für Ihre Codebasis geeignet ist.

Ich würde auch empfehlen, dass Sie " Effektiv mit Legacy-Code arbeiten " lesen, der eine Fülle nützlicher Tipps enthält, um ein größeres Refactoring zum Erfolg zu führen.

Gary Rowe
quelle
Vielen Dank. Ich denke in einigen Fällen über diesen Ansatz nach. Was tun Sie, wenn der Konstruktor von MyObject Parameter erfordert, die nur innerhalb Ihrer Klasse verfügbar sind? Oder wenn Sie zu Lebzeiten Ihrer Klasse mehr als ein MyObject erstellen müssen? Müssen Sie eine MyObjectFactory injizieren?
JW01,
@ JW01 Sie können den Getter-Ersteller-Code so konfigurieren, dass er den internen Status Ihrer Klasse liest und ihn einfach einfügt. Das Erstellen von mehr als einer Instanz im Laufe des Lebens ist schwierig zu orchestrieren. Wenn Sie in diese Situation geraten, würde ich eher eine Neugestaltung als eine Umgehung vorschlagen. Machen Sie Ihre Getter nicht zu komplex, sonst werden sie zu einem Mühlstein um Ihren Hals. Ihr Ziel ist es, den Refactoring-Prozess zu vereinfachen. Wenn es zu schwierig erscheint, wechseln Sie den Bereich, um ihn dort zu beheben.
Gary Rowe
Das ist ein Singleton. Benutze einfach keine Singletons O_O
GameDeveloper 21.07.13
@DarioOO Eigentlich ist es ein sehr schlechter Singleton. Eine bessere Implementierung würde eine Aufzählung nach Josh Bloch verwenden. Singletons sind ein gültiges Entwurfsmuster und haben ihre Verwendung (teure Einzelanfertigungen usw.).
Gary Rowe