Diese Frage hat mich einige Tage lang beschäftigt, und es scheint, als würden sich mehrere Praktiken widersprechen.
Beispiel
Iteration 1
public class FooDao : IFooDao
{
private IFooConnection fooConnection;
private IBarConnection barConnection;
public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
{
this.fooConnection = fooConnection;
this.barConnection = barConnection;
}
public Foo GetFoo(int id)
{
Foo foo = fooConection.get(id);
Bar bar = barConnection.get(foo);
foo.bar = bar;
return foo;
}
}
Wenn ich dies teste, fälsche ich IFooConnection und IBarConnection und verwende Dependency Injection (DI), wenn ich FooDao instanziiere.
Ich kann die Implementierung ändern, ohne die Funktionalität zu ändern.
Iteration 2
public class FooDao : IFooDao
{
private IFooBuilder fooBuilder;
public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
{
this.fooBuilder = new FooBuilder(fooConnection, barConnection);
}
public Foo GetFoo(int id)
{
return fooBuilder.Build(id);
}
}
Jetzt werde ich diesen Builder nicht schreiben, aber stellen Sie sich vor, er macht dasselbe wie FooDao zuvor. Dies ist nur ein Refactoring, daher ändert dies offensichtlich nichts an der Funktionalität, und so besteht mein Test immer noch.
IFooBuilder ist intern, da es nur für die Arbeit in der Bibliothek vorhanden ist, dh nicht Teil der API.
Das einzige Problem ist, dass ich die Abhängigkeitsinversion nicht mehr einhalte. Wenn ich dies umschreibe, um das Problem zu beheben, sieht es möglicherweise so aus.
Iteration 3
public class FooDao : IFooDao
{
private IFooBuilder fooBuilder;
public FooDao(IFooBuilder fooBuilder)
{
this.fooBuilder = fooBuilder;
}
public Foo GetFoo(int id)
{
return fooBuilder.Build(id);
}
}
Das sollte es tun. Ich habe den Konstruktor geändert, daher muss mein Test geändert werden, um dies zu unterstützen (oder umgekehrt). Dies ist jedoch nicht mein Problem.
Damit dies mit DI funktioniert, müssen FooBuilder und IFooBuilder öffentlich sein. Dies bedeutet, dass ich einen Test für FooBuilder schreiben sollte, da dieser plötzlich Teil der API meiner Bibliothek wurde. Mein Problem ist, dass Clients der Bibliothek sie nur über mein beabsichtigtes API-Design IFooDao verwenden sollten und meine Tests als Clients fungieren. Wenn ich Dependency Inversion nicht folge, sind meine Tests und die API sauberer.
Mit anderen Worten, alles, was mich als Client oder als Test interessiert, ist, das richtige Foo zu erhalten, nicht wie es aufgebaut ist.
Lösungen
Sollte es mich einfach nicht interessieren, schreibe die Tests für FooBuilder, obwohl es nur öffentlich ist, DI zu gefallen? - Unterstützt Iteration 3
Sollte ich erkennen, dass das Erweitern der API ein Nachteil von Dependency Inversion ist, und mir klar sein, warum ich mich hier dagegen entschieden habe? - Unterstützt Iteration 2
Mache ich zu viel Wert auf eine saubere API? - Unterstützt Iteration 3
EDIT: Ich möchte klarstellen, dass mein Problem nicht "Wie teste ich Interna?" Ist, sondern so etwas wie "Kann ich es intern behalten und trotzdem DIP einhalten, und sollte ich?".
quelle
IFooBuilder
öffentlich sein?FooBuilder
müssen nur über eine Art "Get Default Implementation" -Methode für das DI-System verfügbar gemacht werden, die eine zurückgibtIFooBuilder
und daher intern bleiben kann.public
Bibliotheks-APIs undpublic
Tests machen". Oder in der anderen Form "Ich möchte Methoden privat halten, um zu vermeiden, dass sie in der API angezeigt werden. Wie teste ich diese privaten Methoden?". Die Antworten lauten immer "Live mit der breiteren öffentlichen API", "Live mit Tests über die öffentliche API" oder "Methodeninternal
erstellen und für den Testcode sichtbar machen".Antworten:
Ich würde gehen mit:
In den SOLID Principles of OO und Agile Design (a um 1:08:55) sagt Onkel Bob jedoch, dass seine Regel bezüglich der Abhängigkeitsinjektion lautet, dass nicht alles injiziert wird, sondern nur an strategischen Stellen. (Er erwähnt auch, dass die Themen Abhängigkeitsinversion und Abhängigkeitsinjektion gleich sind).
Das heißt, die Abhängigkeitsinversion soll nicht auf alle Klassenabhängigkeiten in einem Programm angewendet werden. Sie sollten dies an strategischen Standorten tun (wenn es sich auszahlt (oder möglicherweise auszahlt)).
quelle
Ich werde einige Erfahrungen / Beobachtungen verschiedener Codebasen teilen, die ich gesehen habe. NB Ich befürworte keinen bestimmten Ansatz, sondern teile Ihnen nur mit, wohin dieser Code führt:
Vor DI und automatisierten Tests war die akzeptierte Weisheit, dass etwas auf den erforderlichen Umfang beschränkt werden sollte. Heutzutage ist es jedoch nicht ungewöhnlich, dass Methoden, die intern belassen werden könnten , zum leichteren Testen veröffentlicht werden (da dies normalerweise in einer anderen Assembly geschieht). Hinweis: Es gibt eine Richtlinie, mit der interne Methoden offengelegt werden sollen, aber dies ist mehr oder weniger dasselbe.
DI verursacht zweifellos einen Overhead mit zusätzlichem Code.
Da DI Frameworks tun zusätzlichen Code einzuführen, habe ich eine Verschiebung in Richtung Code festgestellt , dass im DI - Stil geschrieben , während nicht DI nicht verwendet per se also die D von SOLID.
Zusammenfassend:
Zugriffsmodifikatoren werden manchmal gelockert, um das Testen zu vereinfachen
DI führt zusätzlichen Code ein
In Anbetracht von 1 & 2 sind einige Entwickler der Ansicht, dass dies ein zu großes Opfer ist, und entscheiden sich daher einfach dafür, zunächst von Abstraktionen abhängig zu sein, mit der Option, DI später nachzurüsten.
quelle
Ich kaufe mir nicht besonders die Vorstellung, dass Sie private Methoden (mit welchen Mitteln auch immer) offenlegen müssen, um geeignete Tests durchzuführen. Ich würde auch vorschlagen, dass Ihre Client-API nicht mit der öffentlichen Methode Ihres Objekts identisch ist, z. B. hier Ihre öffentliche Schnittstelle:
und es gibt keine Erwähnung von dir
FooBuilder
In Ihrer ursprünglichen Frage würde ich den dritten Weg mit Ihrem nehmen
FooBuilder
. Ich würde Sie vielleichtFooDao
mit einem Mock testenFooDao
und mich so auf Ihre Funktionalität konzentrieren (Sie können immer einen echten Builder einfügen, wenn es einfacher ist), und ich würde separate Tests um Sie herum durchführenFooBuilder
.(Ich bin überrascht, dass in dieser Frage / Antwort nicht auf Spott hingewiesen wurde - dies geht Hand in Hand mit dem Testen von DI / IoC-gemusterten Lösungen.)
Re. dein Kommentar:
Ich denke nicht, dass dies die API erweitert, die Sie Ihren Kunden präsentieren. Sie können die von Ihren Clients verwendete API über Schnittstellen einschränken (wenn Sie dies wünschen). Ich würde auch vorschlagen, dass Ihre Tests nicht nur Ihre öffentlich zugänglichen Komponenten umgeben sollten. Sie sollten alle Klassen (Implementierungen) umfassen, die diese API darunter unterstützen. Beispiel: Hier ist die REST-API für das System, an dem ich gerade arbeite:
(im Pseudocode)
Ich habe eine API-Methode und Tausende von Tests - die überwiegende Mehrheit davon befasst sich mit den Objekten, die dies unterstützen.
quelle
Warum möchten Sie in diesem Fall dem DI folgen, wenn nicht zu Testzwecken? Wenn nicht nur Ihre öffentliche API, sondern auch Ihre Tests ohne
IFooBuilder
Parameter (wie Sie geschrieben haben) wirklich sauberer sind , ist es nicht sinnvoll, eine solche Klasse einzuführen. DI ist kein Selbstzweck, es sollte Ihnen helfen, Ihren Code testbarer zu machen. Wenn Sie keine automatisierten Tests für diese bestimmte Abstraktionsebene schreiben möchten, wenden Sie DI nicht auf dieser Ebene an.Nehmen wir jedoch für einen Moment an, Sie möchten DI anwenden, um Unit-Tests für den
FooDao
Konstruktor isoliert ohne den Erstellungsprozess erstellen zu können, möchten aber dennoch keinenFooDao(IFooBuilder fooBuilder)
Konstruktor in Ihrer öffentlichen API, sondern nur einen KonstruktorFooDao(IFooConnection fooConnection, IBarConnection barConnection)
. Geben Sie dann beide an, die erstere als "intern", die letztere als "öffentlich":Jetzt können Sie
FooBuilder
undIFooBuilder
intern behalten und anwendenInternalsVisibleTo
, um sie durch Unit-Tests zugänglich zu machen.quelle