Die Abhängigkeitsinversion erweitert die API und führt zu unnötigen Tests

8

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?".

Chris Wohlert
quelle
Ich kann meine Frage bearbeiten, um die Tests hinzuzufügen, wenn Sie möchten.
Chris Wohlert
1
"FooBuilder und IFooBuilder müssen öffentlich sein". Muss doch nur IFooBuilderöffentlich sein? FooBuildermüssen nur über eine Art "Get Default Implementation" -Methode für das DI-System verfügbar gemacht werden, die eine zurückgibt IFooBuilderund daher intern bleiben kann.
David Arno
Ich denke, das könnte eine Lösung sein.
Chris Wohlert
2
Dies wurde hier bei Programmierern mindestens ein Dutzend Mal mit unterschiedlichen Begriffen gefragt: "Ich möchte einen Unterschied zwischen publicBibliotheks-APIs und publicTests 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 "Methoden internalerstellen und für den Testcode sichtbar machen".
Doc Brown
Ich würde den ctor wahrscheinlich nicht ersetzen, sondern einen neuen einführen und sie miteinander verketten.
RubberDuck

Antworten:

3

Ich würde gehen mit:

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

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)).

Robert Jørgensgaard Engdahl
quelle
2
Dieser Kommentar von Bob ist perfekt. Vielen Dank, dass Sie dies geteilt haben.
Chris Wohlert
5

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:

Sollte es mich einfach nicht interessieren, schreibe die Tests für FooBuilder, obwohl es nur öffentlich ist, DI zu gefallen

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.

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?

DI verursacht zweifellos einen Overhead mit zusätzlichem Code.

Mache ich zu viel Wert auf eine saubere API?

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:

  1. Zugriffsmodifikatoren werden manchmal gelockert, um das Testen zu vereinfachen

  2. 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.

Robbie Dee
quelle
4
Aus diesem Grund entscheiden sich einige Entwickler für das InternalsVisibleTo- Attribut für interne Methoden, anstatt sie als öffentlich verfügbar zu machen.
Robbie Dee
1
"Heutzutage ist es jedoch nicht ungewöhnlich, dass Methoden, die intern gelassen werden könnten, veröffentlicht werden, um das Testen zu vereinfachen (da dies normalerweise in einer anderen Assembly geschieht)." - wirklich?
Brian Agnew
3
@ BrianAgnew leider ja. Es ist ein Anti-Pattern zusammen mit "Wir müssen 100% Unit-Test-Abdeckung haben", einschließlich Getter und Setter :-( Warum sind Codierer heutzutage so
undenkbar?!
2
@ ChrisWohlert einfache Lösung. Ersetzen Sie den Ctor nicht. Fügen Sie eine neue hinzu. Verkette sie. Lassen Sie Ihren vorhandenen Test an Ort und Stelle. Es testet implizit Ihren Builder und Ihr Code wird weiterhin abgedeckt. Schreiben Sie Tests nur, wenn dies wertvoll ist. Es gibt einen Punkt, an dem Sie eine sinkende Rendite für Ihre Investition erzielen.
RubberDuck
1
@ ChrisWohlert Es ist absolut nichts Falsches daran, einen Convenience-Konstruktor bereitzustellen . Sie injizieren immer noch die wahren Abhängigkeiten der Klasse . Wie wurde DI gemacht, bevor all diese "ausgefallenen" IoC-Container-Frameworks entstanden sind?
RubberDuck
0

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:

interface IFooDao {
   public Foo GetFoo(int id);
}

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 vielleicht FooDaomit einem Mock testen FooDaound 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ühren FooBuilder.

(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:

Wie bereits erwähnt, bietet der Builder keine neuen Funktionen, sodass ich sie nicht testen muss. DIP macht sie jedoch zu einem Teil der API, wodurch ich gezwungen bin, sie zu testen.

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)

void doSomeCalculation(json : CalculationRequestObject);

Ich habe eine API-Methode und Tausende von Tests - die überwiegende Mehrheit davon befasst sich mit den Objekten, die dies unterstützen.

Brian Agnew
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 glaube nicht, dass jemand dies befürwortet ...
Robbie Dee
Aber Sie sagten oben: "Zugriffsmodifikatoren werden manchmal gelockert, um das Testen zu vereinfachen"?
Brian Agnew
Interna können für die Testassembly verfügbar gemacht werden, und die implizite Schnittstellenimplementierung erstellt öffentliche Methoden. Ich habe nie befürwortet, private Methoden aufzudecken ...
Robbie Dee
In meinem PoV bedeutet "Interna können der Testbaugruppe ausgesetzt werden" dasselbe. Ich bin nicht davon überzeugt, dass das Testen von Interna (ob als privat oder anderweitig gekennzeichnet) eine lohnende Übung ist, es sei denn, es handelt sich um einen Regressionstest, bei dem schlecht strukturierter Code überarbeitet wird
Brian Agnew,
Hallo Brian, ich würde, wie Sie, den IFooBuilder fälschen, wenn er auf FooDao analysiert wird, aber heißt das nicht, dass ich einen weiteren Test für die Implementierung von IFooBuilder benötigen würde? Wir nähern uns endlich dem eigentlichen Problem und nicht, ob ich Interna aufdecken soll oder nicht.
Chris Wohlert
0

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 IFooBuilderParameter (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 FooDaoKonstruktor isoliert ohne den Erstellungsprozess erstellen zu können, möchten aber dennoch keinen FooDao(IFooBuilder fooBuilder)Konstruktor in Ihrer öffentlichen API, sondern nur einen Konstruktor FooDao(IFooConnection fooConnection, IBarConnection barConnection). Geben Sie dann beide an, die erstere als "intern", die letztere als "öffentlich":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Jetzt können Sie FooBuilderund IFooBuilderintern behalten und anwenden InternalsVisibleTo, um sie durch Unit-Tests zugänglich zu machen.

Doc Brown
quelle
Der erste Teil Ihrer Frage ist genau das, wonach ich suche. Eine Antwort auf die Frage: "Soll ich hier DIP verwenden". Ich möchte diesen zweiten Konstruktor allerdings nicht. Wenn ich DIP nicht einhalte, muss ich IFooBuilder nicht öffentlich machen, und ich kann aufhören, es zu fälschen / zu testen. Kurz gesagt, Sie sagen mir, ich soll mich an Iteration 2 halten.
Chris Wohlert
Ehrlich gesagt, wenn Sie alles aus "Jedoch" entfernen, werde ich die Antwort akzeptieren. Ich denke, der erste Teil erklärt sehr gut, wann und warum ich DI verwenden sollte / nicht.
Chris Wohlert
Ich möchte nur die Einschränkung hinzufügen, dass es bei DI nicht nur um das Testen geht - es vereinfacht lediglich den Austausch einer Konkretion gegen eine andere. Ob Sie dies hier brauchen, können nur Sie beantworten. Wie 17 von 26 wunderbar sagen - irgendwo zwischen YAGNI & PYIAC.
Robbie Dee
@ChrisWohlert: Ich werde den 2. Teil nicht entfernen, nur weil Sie ihn nicht gerne für Ihre Situation anwenden. Sie brauchen keine Tests auf dieser Ebene - gut, wenden Sie Teil 1 an, verwenden Sie DI hier nicht. Sie möchten Tests durchführen und vermeiden, dass bestimmte Teile in der öffentlichen API enthalten sind. Gut, Sie benötigen zwei Einstiegspunkte in Ihrem Code mit unterschiedlichen Zugriffsmodifikatoren. Wenden Sie also Teil 2 an. Jeder kann die Lösung auswählen, die am besten zu ihm passt.
Doc Brown
Sicher, jeder kann die gewünschte Lösung auswählen, aber Sie haben die Frage falsch verstanden, wenn Sie glauben, ich wollte den Builder testen. Das habe ich versucht loszuwerden.
Chris Wohlert