Ich versuche, ein sehr großes, sehr altes Projekt testbar zu machen.
Wir haben eine Reihe von statisch verfügbaren Diensten, die die meisten unserer Codes verwenden. Das Problem ist, dass diese schwer zu verspotten sind. Sie waren einmal Singletons. Jetzt sind sie Pseudo-Singletons - dieselbe statische Schnittstelle, aber die Funktionen delegieren an ein Instanzobjekt, das ausgeschalten werden kann. So:
%Vor%Jetzt in meinem Komponententest:
%Vor%So weit, so gut. In prod benötigen wir nur die eine Implementierung. Aber Tests laufen parallel und benötigen möglicherweise andere Mocks, also habe ich Folgendes gemacht:
%Vor%Und dann:
%Vor%Wir haben diesen Weg eingeschlagen, weil es ein riesiges Projekt ist, um diese Dienste in jede Klasse zu integrieren, die sie braucht. Gleichzeitig sind dies Universaldienste innerhalb unserer Codebasis, von denen die meisten Funktionen abhängen.
Ich verstehe, dass dieses Muster in dem Sinne schlecht ist, dass es Abhängigkeiten verbirgt, im Gegensatz zur Konstruktorinjektion, die Abhängigkeiten explizit macht.
Aber die Vorteile sind:
- Wir können den Komponententest sofort starten, anstatt einen 3-monatigen Refactor zu machen und dann den Komponententest durchzuführen.
- Wir haben immer noch Globals, aber das scheint strikt besser zu sein als wo wir waren.
Obwohl unsere Abhängigkeiten immer noch implizit sind, würde ich argumentieren, dass dieser Ansatz strikt besser ist als das, was wir hatten. Abgesehen von den versteckten Abhängigkeiten, ist das in gewisser Weise schlimmer als die Verwendung eines richtigen DI-Containers? Auf welche Probleme werde ich stoßen?
Es ist ein Service-Locator, der schlecht ist . Aber das weißt du schon. Wenn Ihre Codebasis so umfangreich ist, starten Sie eine Teilmigration. Registrieren Sie die Singleton-Instanzen mit dem Container, und starten Sie den Konstruktor, indem Sie sie injizieren, wenn Sie eine Klasse in Ihrem Code berühren. Dann können Sie die meisten Teile in einem (hoffentlich) funktionierenden Zustand lassen und überall sonst die Vorteile von DI nutzen.
Idealerweise sollten die Teile ohne DI mit der Zeit schrumpfen. Und Sie können sofort mit dem Testen beginnen.
Dies wird als Umgebungskontext bezeichnet. Es ist nichts falsch daran, einen Umgebungskontext zu verwenden, wenn dieser korrekt verwendet und implementiert wird. Es gibt einige Voraussetzungen, wenn ein Umgebungskontext verwendet werden kann:
null
nicht zugewiesen werden kann. (Verwenden Sie stattdessen eine Null-Implementierung ) Für Querschneidereien, die keine Werte z. Logging sollten Sie Interception bevorzugen. Für andere Abhängigkeiten, die keine Querschneidereien sind, sollten Sie eine Konstruktorinjektion durchführen.
Ihre Implementierung hat jedoch einige Probleme (verhindert nicht die Zuweisung von null, Benennung, keine Vorgabe). Hier ist, wie Sie es umsetzen können:
%Vor%Ein Umgebungskontext hat den Vorteil, dass Sie Ihre API nicht für Querschneidereien belasten.
Aber auf der anderen Seite in Bezug auf Tests können Sie Tests abhängig werden. Z.B. Wenn Sie vergessen haben, Ihren Mock für einen Test einzurichten, wird er korrekt ausgeführt, wenn der Mock zuvor von einem anderen Test eingerichtet wurde. Aber wenn es eigenständig oder in einer anderen Reihenfolge ausgeführt wird, wird es fehlschlagen. Es erschwert das Testen.
Ich denke, was du machst, ist nicht schlecht. Sie versuchen, Ihre Codebasis testbar zu machen, und der Trick besteht darin, dies in kleinen Schritten zu tun. Sie erhalten denselben Rat beim Lesen von Effektiver Umgang mit altem Code . Nachteil dessen, was Sie tun, ist jedoch, dass Sie, sobald Sie mit der Dependency-Injektion beginnen, Ihre Codebasis erneut refaktorieren müssen. Aber noch wichtiger: Sie müssen eine Menge Testcode ändern.
Ich stimme Alex zu. Verwenden Sie lieber die Konstruktorinjektion, anstatt einen Umgebungskontext zu verwenden. Sie müssen Ihre gesamte Codebasis nicht direkt umgestalten, aber die Konstruktorinjektion wird den Aufruf-Stack "aufblasen", und Sie müssen einen "Schnitt" vornehmen, um zu verhindern, dass es aufblubbert, weil Sie dies zwingt viele Änderungen in der Codebasis.
Ich arbeite derzeit an einer Legacy-Code-Basis und kann keinen DI-Container (den Schmerz) verwenden. Dennoch benutze ich Konstruktorinjektion, wo ich kann, was manchmal bedeutet, dass ich zu Armens Abhängigkeitsinjektion bei einigen Typen. Dies ist der Trick, den ich benutze, um die "Konstruktor-Injektionsblase" zu stoppen. Dennoch ist dies viel besser als die Verwendung eines Umgebungskontextes. Die DI des armen Mannes ist suboptimal, erlaubt dir aber immer noch, richtige Komponententests zu schreiben und macht es später viel einfacher, diesen Standardkonstruktor später auszubrechen.
Abhängigkeitsinjektion und die Verwendung eines DI-Containers sind wirklich getrennte Unternehmungen, obwohl man natürlich zu der anderen führt. Die Verwendung eines DI-Containers impliziert, dass der Code eine bestimmte Struktur aufweist. Solch eine Struktur ist wahrscheinlich einfacher zu lesen, und es ist sicherlich einfacher, ohne tiefere Kenntnis der versteckten Abhängigkeiten zu arbeiten, und ist daher wartungsfreundlicher.
Da Sie nun nicht mehr auf Konkretionen angewiesen sind, haben Sie eine Form der Inversion der Kontrolle implementiert. Ich denke, das ist ein besseres Design und stellt einen guten Ausgangspunkt dar, um den Code testbarer zu machen. Es klingt, als ob du von diesem Schritt einen sofortigen Wert hast.
Wäre es besser, explizite Abhängigkeiten zu haben als implizite Abhängigkeiten (dh DI vs. Umgebungskontext)? Ich würde geneigt sein, Ja zu sagen, aber es hängt wirklich von den Kosten gegen den Nutzen ab. Der Nutzen hängt von Dingen wie der Kosten für die Einführung von Bugs, wie viel Abwanderung Sie wahrscheinlich in den Code sehen, wie schwer es ist, zu debuggen, wer es wird, was seine erwartete Lebensdauer ist, etc ..
Der globale veränderbare statische Zustand ist immer schlecht. Einige kluge Köpfe könnten entscheiden, dass sie die Implementierung eines globalen Service austauschen müssen, während sie einen Anruf tätigen, und ihn anschließend ersetzen. Dies kann sehr schief gehen, wenn sie danach nicht aufräumen. Das mag ein dummes Beispiel sein, aber solche unbeabsichtigten Nebenwirkungen sind immer schlecht, daher ist es besser, sie vollständig zu eliminieren. Sie können sie mit Disziplin und Wachsamkeit verhindern, aber es ist schwieriger.
Tags und Links .net unit-testing dependency-injection