Ist implizit abhängig von reinen Funktionen schlecht (insbesondere zum Testen)?

8

Um den Titel ein wenig zu erweitern, versuche ich zu einer Schlussfolgerung zu gelangen, ob es notwendig ist, reine Funktionen, von denen eine andere Funktion oder Klasse abhängt, explizit zu deklarieren (dh zu injizieren).

Ist ein bestimmter Code weniger testbar oder schlechter konzipiert, wenn er reine Funktionen verwendet, ohne danach zu fragen? Ich mag zu einem Abschluss über die Angelegenheit bekommen, für jede Art von reiner Funktion von einfachen, nativen Funktionen (zB max(), min()- unabhängig von der Sprache) , um benutzerdefinierte, kompliziertere, die wiederum implizit auf anderen reinen Funktionen abhängen.

Die übliche Sorge ist, dass wenn ein Code nur eine Abhängigkeit direkt verwendet, Sie diese nicht isoliert testen können, sondern gleichzeitig alle Dinge testen, die Sie stillschweigend mitgebracht haben. Aber das fügt ziemlich viel Boilerplate hinzu, wenn Sie es für jede kleine Funktion tun müssen, also frage ich mich, ob dies immer noch für reine Funktionen gilt und warum oder warum nicht.

DanielM
quelle
2
Ich jedenfalls verstehe diese Frage nicht. Was macht es aus, wenn eine Funktion für DI-Zwecke rein ist?
Telastyn
Sie schlagen also vor, reine Funktionen, unreine Funktionen und Klassen gleich zu injizieren, unabhängig von ihrer Größe, ihren transitiven Abhängigkeiten oder irgendetwas. Könnten Sie das vielleicht näher erläutern?
DanielM
Nein, ich verstehe einfach nicht, was Sie fragen. Funktionen werden selten in irgendetwas injiziert, und wenn dies der Fall ist, kann der Verbraucher seine Reinheit nicht garantieren.
Telastyn
1
Sie können jede Funktionalität injizieren, aber oft ist es nicht sinnvoll, diese Funktionen in einem Test zu verspotten, was bedeutet, dass das Injizieren keinen Wert hat. Da Operatoren im Grunde genommen statische Funktionen sind, könnte man sich entscheiden, den Operator + einzufügen und ihn in einem Test zu verspotten. Wenn das keinen Wert hat, würden Sie das nicht tun.
user2180613
1
Auch die Betreiber waren mir in den Sinn gekommen. Es wäre sicherlich unpraktisch, aber der Punkt, den ich zu finden versuchte, ist, wie ich die Entscheidung treffen soll, was injiziert werden soll und was nicht. Mit anderen Worten, wie man entscheidet, wann es Wert gibt und wann nicht. Inspiriert von @ Goyos Antwort denke ich jetzt, dass ein gutes Kriterium darin besteht, alles, was nicht ein wesentlicher Bestandteil des Systems ist, zu injizieren, um das System und seine Abhängigkeiten lose miteinander zu verbinden. und im Gegensatz dazu verwenden Sie einfach Dinge (also injizieren Sie sie nicht), die wirklich Teil der Systemidentität sind und mit denen das System in hohem Maße zusammenhält.
DanielM

Antworten:

10

Nein, das ist nicht schlecht

Den Tests, die Sie schreiben, sollte es egal sein, wie eine bestimmte Klasse oder Funktion implementiert wird. Vielmehr sollte sichergestellt werden, dass sie die gewünschten Ergebnisse liefern, unabhängig davon, wie genau sie implementiert sind.

Betrachten Sie als Beispiel die folgende Klasse:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

Sie möchten die Funktion 'Round' testen, um sicherzustellen, dass sie das zurückgibt, was Sie erwarten, unabhängig davon, wie die Funktion tatsächlich implementiert ist. Sie würden wahrscheinlich einen Test schreiben, der dem folgenden ähnlich ist;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

Unter Testgesichtspunkten ist es Ihnen egal, wie Ihre Coord2d :: Round () -Funktion das zurückgibt, was Sie erwarten.

In einigen Fällen kann das Injizieren einer reinen Funktion eine wirklich hervorragende Möglichkeit sein, eine Klasse testbarer oder erweiterbarer zu machen.

In den meisten Fällen, wie bei der obigen Funktion Coord2d :: Round (), muss jedoch keine Min / Max / Floor / Ceil / Trunc-Funktion eingefügt werden. Die Funktion ist so konzipiert, dass ihre Werte auf die nächste ganze Zahl gerundet werden. Die Tests, die Sie schreiben, sollten überprüfen, ob die Funktion dies tut.

Wenn Sie Code testen möchten, von dem eine Klassen- / Funktionsimplementierung abhängt, können Sie dies tun, indem Sie einfach Tests für die Abhängigkeit schreiben.

Zum Beispiel, wenn die Funktion Coord2d :: Round () wie folgt implementiert wurde ...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

Wenn Sie die Bodenfunktion testen möchten, können Sie dies in einem separaten Komponententest tun.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)
Ryan
quelle
Alles, was Sie gesagt haben, bezieht sich auf reine Funktionen, richtig? Wenn ja, welchen Unterschied macht es bei nicht reinen oder Klassen? Ich meine, Sie könnten einfach alles hart codieren und dieselbe Logik anwenden ("alles, von dem ich abhängig bin, ist bereits getestet"). Und eine andere Frage: Könnten Sie näher erläutern, wann es brillant wäre, reine Funktionen zu injizieren, und warum? Vielen Dank!
DanielM
3
@DanielM Absolut - Unit-Tests sollten eine isolierte Einheit sinnvollen Verhaltens abdecken - wenn die Gültigkeit des Verhaltens der "Einheit" streng von der Rückgabe des Maximums von etwas abhängt, ist das Abstrahieren von "Max" nicht sinnvoll und verringert tatsächlich den Nutzen des Tests auch. Wenn ich dagegen das "Maximum" von allem zurückgebe, was von einem Anbieter stammt, ist es nützlich, den Anbieter zu stubben, da das, was er tut, nicht direkt für das beabsichtigte Verhalten dieses Geräts relevant ist und dessen Richtigkeit überprüft werden kann anderswo. Denken Sie daran - "Einheit"! = "Klasse."
Ameise P
2
Letztendlich ist es jedoch normalerweise kontraproduktiv, solche Dinge zu überdenken. Hier kommt Test-First zum Tragen. Definieren Sie das isolierte Verhalten, das Sie testen möchten, und schreiben Sie dann den Test. Die Entscheidung, was injiziert werden soll und was nicht, wird für Sie getroffen.
Ameise P
4

Ist implizit abhängig von reinen Funktionen schlecht

Aus Testsicht - Nein, aber nur bei reinen Funktionen, wenn die Funktion immer dieselbe Ausgabe für dieselbe Eingabe zurückgibt.

Wie testen Sie Geräte, die explizit injizierte Funktionen verwenden?
Sie werden eine verspottete (gefälschte) Funktion einfügen und überprüfen, ob die Funktion mit korrekten Argumenten aufgerufen wurde.

Da wir jedoch eine reine Funktion haben, die immer dasselbe Ergebnis für dieselben Argumente zurückgibt, ist die Überprüfung auf Eingabeargumente für die Überprüfung des Ausgabeergebnisses gleich.

Mit reinen Funktionen müssen Sie keine zusätzlichen Abhängigkeiten / Zustände konfigurieren.

Als zusätzlichen Vorteil erhalten Sie eine bessere Kapselung und testen das tatsächliche Verhalten des Geräts.
Und aus Sicht des Testens sehr wichtig: Sie können den internen Code der Einheit frei ändern, ohne die Tests zu ändern. Sie möchten beispielsweise ein weiteres Argument für die reine Funktion hinzufügen - mit explizit injizierter Funktion (in den Tests verspottet), die Sie benötigen Ändern Sie die Testkonfiguration, wobei Sie mit der implizit verwendeten Funktion nichts tun.

Ich kann mir eine Situation vorstellen, in der Sie reine Funktion einspeisen müssen - wenn Sie den Verbrauchern anbieten möchten, das Verhalten des Geräts zu ändern.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

Für die oben beschriebene Methode erwarten Sie, dass die Rabattberechnung von den Verbrauchern geändert werden kann. Daher müssen Sie das Testen der Logik von den Komponententests für die CalcualteTotalPriceMethode ausschließen.

Fabio
quelle
Das Injizieren der Funktion bedeutet nicht, dass Sie auf Funktionsaufrufe testen müssen. Eigentlich würde ich behaupten, dass das SUT das Richtige tut (in Bezug auf den beobachtbaren Zustand), wenn es den Schein bestanden hat. OTOH, der eine Abhängigkeit raucht, die nicht Teil der SUT-Schnittstelle ist, wäre in meinen Augen seltsam.
Sie auf, Monica
@Goyo, Sie konfigurieren mock so, dass der erwartete Wert nur für bestimmte Eingaben zurückgegeben wird. Ihr Mock "schützt" also, dass korrekte Eingabeargumente bereitgestellt wurden.
Fabio
4

In einer idealen Welt der SOLID OO-Programmierung würden Sie jedes externe Verhalten injizieren. In der Praxis werden Sie immer einige einfache (oder nicht so einfache) Funktionen direkt verwenden.

Wenn Sie das Maximum einer Reihe von Zahlen berechnen, wäre es wahrscheinlich übertrieben, eine maxFunktion zu injizieren und sie in Komponententests zu verspotten. Normalerweise ist es Ihnen egal, Ihren Code von einer bestimmten Implementierung zu entkoppeln maxund isoliert zu testen.

Wenn Sie etwas Komplexes tun, z. B. einen Pfad mit minimalen Kosten in einem Diagramm finden, sollten Sie die konkrete Implementierung aus Gründen der Flexibilität einfügen und sie in Komponententests verspotten, um eine bessere Leistung zu erzielen. In diesem Fall kann es sich lohnen, den Pfadfindungsalgorithmus von dem Code zu entkoppeln, der ihn verwendet.

Es kann keine Antwort für "irgendeine Art von reiner Funktion" geben, Sie müssen entscheiden, wo die Linie gezogen werden soll. Bei dieser Entscheidung sind zu viele Faktoren beteiligt. Am Ende müssen Sie die Vorteile gegen die Probleme abwägen, die Ihnen die Entkopplung bereitet, und das hängt von Ihnen und Ihrem Kontext ab.

Ich sehe in den Kommentaren, dass Sie nach dem Unterschied zwischen injizierenden Klassen (tatsächlich injizieren Sie Objekte) und Funktionen fragen. Es gibt keinen Unterschied, nur Sprachfunktionen lassen es anders aussehen. Aus abstrakter Sicht unterscheidet sich das Aufrufen einer Funktion nicht vom Aufrufen der Methode eines Objekts, und Sie injizieren (oder nicht) die Funktion aus den gleichen Gründen, aus denen Sie das Objekt injizieren (oder nicht): um dieses Verhalten vom aufrufenden Code zu entkoppeln und die Fähigkeit, verschiedene Implementierungen des Verhaltens zu verwenden und woanders zu entscheiden, welche Implementierung verwendet werden soll.

Grundsätzlich können Sie alle Kriterien, die Sie für die Abhängigkeitsinjektion für gültig halten, unabhängig davon verwenden, ob es sich bei der Abhängigkeit um ein Objekt oder eine Funktion handelt. Abhängig von der Sprache kann es einige Nuancen geben (dh wenn Sie Java verwenden, ist dies kein Problem).

Hör auf, Monica zu schaden
quelle
3
Injizieren hat nichts mit objektorientierter Programmierung zu tun.
Frank Hileman
@FrankHileman Besser? Sie benötigen eine Abhängigkeitsinjektion, um von Abstraktionen abhängig zu sein, da Sie sie nicht instanziieren können.
Hören Sie auf, Monica
Es wäre auf jeden Fall von Vorteil, eine grobe Liste der wichtigsten Faktoren in der Praxis zu haben. Warum interessiert es Sie im Zusammenhang damit nicht, sich mit max()etwas zu verbinden (im Gegensatz zu etwas anderem)?
DanielM
SOLID ist weder "ideal" noch hat es etwas mit objektorientierter Programmierung zu tun.
Frank Hileman
@FrankHileman Wie hat SOLID nichts mit OOP zu tun? Die fünf Prinzipien wurden von Robert Martin in seiner Arbeit aus dem Jahr 2000 in einem Kapitel mit dem Titel Prinzipien des objektorientierten Klassendesigns vorgeschlagen.
NickL
3

Reine Funktionen beeinträchtigen die Testbarkeit von Klassen aufgrund ihrer Eigenschaften nicht:

  • Ihre Ausgabe (oder Fehler / Ausnahme) hängt nur von ihren Eingaben ab.
  • ihre Leistung hängt nicht vom Weltstaat ab;
  • ihr Betrieb verändert den Weltzustand nicht.

Dies bedeutet, dass sie sich ungefähr im selben Bereich befinden wie private Methoden, die vollständig unter der Kontrolle der Klasse stehen, mit dem zusätzlichen Bonus, dass sie nicht einmal vom aktuellen Status der Instanz abhängen (dh "dies"). Die Benutzerklasse "steuert" die Ausgabe, da sie die Eingaben vollständig steuert.

Die von Ihnen angegebenen Beispiele max () und min () sind deterministisch. Random () und currentTime () sind jedoch Prozeduren, keine reinen Funktionen (sie hängen beispielsweise vom Weltzustand außerhalb des Bandes ab / ändern ihn). Diese beiden letzten müssten injiziert werden, um Tests zu ermöglichen.

carlosdavidepto
quelle