Verhalten beim Testen von Einheiten ohne Kopplung an Implementierungsdetails

14

In seinem Vortrag TDD, in dem alles schief gelaufen ist , drängt Ian Cooper Kent Becks ursprüngliche Absicht hinter Unit-Tests in TDD (um Verhaltensweisen zu testen, nicht Methoden für Klassen) und argumentiert, die Tests nicht an die Implementierung zu koppeln.

save X to some data sourceWie können wir bei einem Verhalten wie in einem System mit einem typischen Satz von Diensten und Repositorys das Speichern einiger Daten auf Serviceebene über das Repository testen, ohne den Test an Implementierungsdetails zu koppeln (z. B. das Aufrufen einer bestimmten Methode)? )? Lohnt es sich nicht, diese Art der Kopplung zu vermeiden?

Andy Hunt
quelle
1
Wenn Sie testen möchten, ob Daten im Repository gespeichert wurden, muss der Test tatsächlich das Repository überprüfen, um festzustellen, ob die Daten vorhanden sind, oder? Oder fehlt mir etwas?
Bei meiner Frage ging es eher darum, zu vermeiden, dass die Tests an ein Implementierungsdetail gekoppelt werden, z. B. das Aufrufen einer bestimmten Methode im Repository, oder ob dies wirklich getan werden sollte.
Andy Hunt

Antworten:

8

Ihr spezielles Beispiel ist ein Fall, den Sie normalerweise testen müssen, indem Sie überprüfen, ob eine bestimmte Methode aufgerufen wurde, da dies saving X to data sourcebedeutet , dass Sie mit einer externen Abhängigkeit kommunizieren. Das zu testende Verhalten besteht also darin, dass die Kommunikation wie erwartet erfolgt .

Dies ist jedoch keine schlechte Sache. Die Grenzflächen zwischen Ihrer Anwendung und ihren externen Abhängigkeiten sind keine Implementierungsdetails , sondern werden in der Architektur Ihres Systems definiert. Dies bedeutet, dass sich eine solche Grenze wahrscheinlich nicht ändert (oder wenn dies erforderlich ist, wäre dies die am wenigsten häufige Änderung). Das Koppeln Ihrer Tests an eine repositorySchnittstelle sollte Ihnen daher nicht zu viele Probleme bereiten (wenn dies der Fall ist, sollten Sie berücksichtigen, ob die Schnittstelle der Anwendung keine Verantwortlichkeiten stiehlt).

Berücksichtigen Sie jetzt nur die Geschäftsregeln einer Anwendung, die von der Benutzeroberfläche, den Datenbanken und anderen externen Diensten entkoppelt sind. Hier müssen Sie die Struktur und das Verhalten des Codes ändern können. Hier werden Sie durch Kopplungstests und Implementierungsdetails gezwungen, mehr Testcode als Produktionscode zu ändern, selbst wenn sich das Gesamtverhalten der Anwendung nicht ändert. Hier können wir testen, Stateanstatt Interactionschneller zu arbeiten.

PS: Ich möchte nicht sagen, ob das Testen nach Status oder Interaktionen der einzig wahre Weg für TDD ist. Ich glaube, es geht darum, das richtige Tool für den richtigen Job zu verwenden.

MichelHenrich
quelle
Wenn Sie "Kommunikation mit einer externen Abhängigkeit" erwähnen, beziehen Sie sich auf externe Abhängigkeiten als solche, die außerhalb des zu testenden Geräts oder außerhalb des Systems als Ganzes liegen?
Andy Hunt
Mit "externer Abhängigkeit" meine ich alles, was Sie als Plug-In für Ihre Anwendung betrachten können. Mit Anwendung meine ich die Geschäftsregeln, die unabhängig von jeglichen Details sind, z. B. welches Framework für die Persistenz oder die Benutzeroberfläche verwendet werden soll. Ich denke, Onkel Bob kann es besser erklären, wie in diesem Vortrag: youtube.com/watch?v=WpkDN78P884
MichelHenrich
Ich denke, dies ist der ideale Ansatz, wie der Vortrag sagt, um auf der Basis von "Merkmalen" oder "Verhalten" und einem Test pro Merkmal oder Verhalten (oder der Permutation eines, dh variierenden Parameters) zu testen. Wenn ich jedoch 1 "Happy" -Test für eine Funktion habe, um TDD durchzuführen, bedeutet dies, dass ich für diese Funktion ein einziges Riesen-Commit (und eine Codeüberprüfung) durchführen muss, was eine schlechte Idee ist. Wie würde dies vermieden werden? Schreiben Sie einen Teil dieser Funktion als Test und den gesamten damit verbundenen Code und fügen Sie dann den Rest der Funktion in nachfolgenden Commits schrittweise hinzu.
Jordan
Ich würde wirklich gerne ein reales Beispiel für Tests sehen, die an die Implementierung gekoppelt sind.
PositiveGuy
7

Meine Interpretation dieses Vortrags lautet:

  • Testkomponenten, keine Klassen.
  • Testen Sie Komponenten über ihre Schnittstellenports.

Es wird im Vortrag nicht angegeben, aber ich denke, der angenommene Kontext für den Rat ist ungefähr so:

  • Sie entwickeln ein System für Benutzer, nicht beispielsweise eine Dienstprogrammbibliothek oder ein Framework.
  • Das Ziel des Testens ist es , so viel wie möglich innerhalb eines wettbewerbsfähigen Budgets erfolgreich zu liefern.
  • Komponenten werden in einer einzigen, ausgereiften, wahrscheinlich statisch typisierten Sprache wie C # / Java geschrieben.
  • eine Komponente liegt in der Größenordnung von 10000-50000 Zeilen; ein Maven- oder VS-Projekt, ein OSGI-Plugin usw.
  • Komponenten werden von einem einzelnen Entwickler oder einem eng integrierten Team geschrieben.
  • Sie folgen der Terminologie und Herangehensweise von etwas wie der hexagonalen Architektur
  • An einem Komponentenport verlassen Sie die lokale Sprache und das Typsystem und wechseln zu http / SQL / XML / bytes / ...
  • Um jeden Port werden typisierte Schnittstellen im Java / C # -Sinne eingeschlossen, bei denen Implementierungen auf Switch-Technologien umgeschaltet werden können.

Das Testen einer Komponente ist daher der größtmögliche Umfang, in dem etwas noch vernünftigerweise als Komponententest bezeichnet werden kann. Dies unterscheidet sich stark davon, wie manche Menschen, insbesondere Akademiker, den Begriff verwenden. Es ist nichts anderes als die Beispiele im typischen Tutorial für Unit-Test-Tools. Es entspricht jedoch seinem Ursprung beim Testen von Hardware. Platinen und Module sind einheitlich getestet, keine Drähte und Schrauben. Oder zumindest bauen Sie keine Schein-Boeing, um eine Schraube zu testen ...

Daraus zu extrapolieren und einige meiner eigenen Gedanken einzubringen,

  • Jede Schnittstelle wird entweder eine Eingabe, eine Ausgabe oder ein Mitarbeiter (wie eine Datenbank) sein.
  • Sie testen die Eingabeschnittstellen. Rufen Sie die Methoden auf und bestätigen Sie die Rückgabewerte.
  • Sie verspotten die Ausgabeschnittstellen; Überprüfen Sie, ob die erwarteten Methoden für einen bestimmten Testfall aufgerufen werden.
  • Sie fälschen die Mitarbeiter; bieten eine einfache, aber funktionierende Implementierung

Wenn Sie das richtig und sauber machen, brauchen Sie kaum ein Spottwerkzeug. Es wird nur einige Male pro System verwendet.

Eine Datenbank ist im Allgemeinen ein Kollaborateur, daher wird sie eher gefälscht als verspottet. Es wäre schmerzhaft, dies von Hand umzusetzen. Zum Glück gibt es solche Dinge bereits .

Das grundlegende Testmuster besteht darin, eine Abfolge von Vorgängen auszuführen (z. B. Speichern und erneutes Laden eines Dokuments). Bestätigen Sie, dass es funktioniert. Dies ist das gleiche wie für jedes andere Testszenario. Keine (funktionierende) Implementierungsänderung führt wahrscheinlich dazu, dass ein solcher Test fehlschlägt.

Die Ausnahme besteht darin, dass Datenbankeinträge vom zu testenden System geschrieben, aber nie gelesen werden. zB Überwachungsprotokolle oder ähnliches. Dies sind Ausgänge und sollten daher verspottet werden. Das Testmuster besteht aus einer Abfolge von Operationen. Bestätigen Sie, dass die Überwachungsschnittstelle mit den angegebenen Methoden und Argumenten aufgerufen wurde.

Beachten Sie, dass selbst hier, sofern Sie ein typsicheres Verspottungswerkzeug wie mockito verwenden , das Umbenennen einer Schnittstellenmethode keinen Testfehler verursachen kann. Wenn Sie eine IDE mit geladenen Tests verwenden, wird diese zusammen mit der Umbenennung der Methode überarbeitet. Wenn Sie dies nicht tun, wird der Test nicht kompiliert.

soru
quelle
Können Sie mir ein konkretes Beispiel für einen Schnittstellenport beschreiben / geben?
PositiveGuy
Was ist ein Beispiel für eine Ausgabeschnittstelle? Können Sie im Code spezifisch sein? Gleiches gilt für die Eingabeschnittstelle.
PositiveGuy
Eine Schnittstelle (im Sinne von Java / C #) umschließt einen Port, bei dem es sich um alles handeln kann, was mit der Außenwelt kommuniziert (d / b, Socket, http, ....). Eine Ausgabeschnittstelle verfügt über keine Methoden mit Rückgabewerten, die über den Port von der Außenwelt stammen, sondern nur Ausnahmen oder Äquivalente.
Soru
Eine Eingabeschnittstelle ist das Gegenteil, ein Mitarbeiter ist sowohl Eingabe als auch Ausgabe.
Soru
1
Ich denke, Sie sprechen von einem völlig anderen Designansatz und einer anderen Terminologie als im Video beschrieben. In 90% der Fälle ist ein Repository (dh eine Datenbank) ein Mitarbeiter, keine Eingabe oder Ausgabe. Die Schnittstelle dazu ist also eine Schnittstelle für die Zusammenarbeit.
Soru
0

Mein Vorschlag ist, einen zustandsbasierten Testansatz zu verwenden:

GEGEBEN Wir haben die Test-DB in einem bekannten Zustand

WANN Der Dienst wird mit Argumenten X aufgerufen

DANN Stellen Sie sicher, dass die Datenbank von ihrem ursprünglichen Status in den erwarteten Status geändert wurde, indem Sie schreibgeschützte Repository-Methoden aufrufen und ihre zurückgegebenen Werte überprüfen

Auf diese Weise verlassen Sie sich nicht auf einen internen Algorithmus des Dienstes und können dessen Implementierung überarbeiten, ohne die Tests ändern zu müssen.

Die einzige Kopplung besteht hier in dem Dienstmethodenaufruf und den Repository-Aufrufen, die zum Lesen von Daten aus der Datenbank erforderlich sind, was in Ordnung ist.

Elifarley
quelle