Testen vs Wiederholen Sie sich nicht (DRY)

11

Warum ist es so empfehlenswert, sich durch das Schreiben von Tests zu wiederholen?

Es scheint, dass Tests im Grunde dasselbe wie der Code ausdrücken und daher ein Duplikat (im Konzept, nicht in der Implementierung) des Codes sind. Würde das ultimative Ziel von DRY nicht die Eliminierung des gesamten Testcodes beinhalten?

John Tseng
quelle

Antworten:

24

Ich glaube, das ist ein Missverständnis, wie ich es mir vorstellen kann.

Der Testcode, der den Produktionscode testet, ist überhaupt nicht ähnlich. Ich werde in Python demonstrieren:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Dann wäre ein einfacher Test:

def test_multiply():
    assert multiply(4, 5) == 20

Beide Funktionen haben eine ähnliche Definition, aber beide machen sehr unterschiedliche Dinge. Kein doppelter Code hier. ;-);

Es kommt auch vor, dass Personen doppelte Tests schreiben, die im Wesentlichen eine Zusicherung pro Testfunktion enthalten. Das ist Wahnsinn und ich habe Leute gesehen, die das getan haben. Das ist schlechte Praxis.

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Stellen Sie sich vor, Sie tun dies für mehr als 1000 effektive Codezeilen. Stattdessen testen Sie pro Feature:

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Wenn nun Features hinzugefügt / entfernt werden, muss ich nur noch eine Testfunktion hinzufügen / entfernen.

Sie haben vielleicht bemerkt, dass ich keine forSchleifen angewendet habe . Dies liegt daran, dass es gut ist, einige Dinge zu wiederholen. Wenn ich Schleifen angewendet hätte, wäre der Code viel kürzer. Wenn eine Zusicherung jedoch fehlschlägt, kann dies die Ausgabe verschleiern, die eine mehrdeutige Nachricht anzeigt. Wenn dies der Fall ist, dann wird Ihre Tests weniger nützlich und Sie werden einen Debugger müssen prüfen , wo die Dinge schief gehen.

siebz0r
quelle
8
Eine Behauptung pro Test wird technisch empfohlen, da dies bedeutet, dass mehrere Probleme nicht nur als ein einziger Fehler angezeigt werden. In der Praxis denke ich jedoch, dass eine sorgfältige Zusammenfassung von Behauptungen die Menge an wiederholtem Code reduziert, und ich halte mich fast nie an eine Behauptung pro Testrichtlinie.
Rob Church
@ Pink-Diamond-Square Ich sehe, dass NUnit nicht aufhört zu testen, nachdem eine Behauptung fehlgeschlagen ist (was ich seltsam finde). In diesem speziellen Fall ist es in der Tat besser, eine Behauptung pro Test zu haben. Wenn ein Unit-Testing-Framework den Test nach einer fehlgeschlagenen Zusicherung beendet, sind mehrere Zusicherungen besser.
siebz0r
3
NUnit stoppt nicht die gesamte Testsuite, aber dieser eine Test wird gestoppt, es sei denn, Sie ergreifen Maßnahmen, um dies zu verhindern (Sie können die Ausnahme abfangen, die ausgelöst wird, was gelegentlich nützlich ist). Ich denke, sie machen, wenn Sie Tests schreiben, die mehr als eine Behauptung enthalten, erhalten Sie nicht alle Informationen, die Sie zur Behebung des Problems benötigen. Stellen Sie sich zum Durcharbeiten Ihres Beispiels vor, dass diese Multiplikationsfunktion die Zahl 3 nicht mag. In diesem Fall assert multiply(1,3)würde dies fehlschlagen, aber Sie würden auch nicht den fehlgeschlagenen Testbericht erhalten assert multiply(3,4).
Rob Church
Ich dachte nur, ich würde es ansprechen, weil eine einzige Behauptung pro Test nach dem, was ich in der .net-Welt gelesen habe, die "gute Praxis" und mehrere Behauptungen "pragmatische Verwendung" sind. In der Python-Dokumentation sieht es etwas anders aus, wo das Beispiel def test_shufflezwei Asserts ausführt.
Rob Church
Ich stimme zu und stimme nicht zu: D Hier gibt es eindeutig Wiederholungen: assert multiply(*, *) == *Sie können also eine assert_multiplyFunktion definieren . Im aktuellen Szenario spielt es keine Rolle für die Anzahl der Zeilen und die Lesbarkeit, aber bei längeren Tests können Sie komplizierte Zusicherungen, Geräte, Code zum Generieren von Geräten usw. wiederverwenden. Ich weiß nicht, ob dies eine bewährte Methode ist, aber normalerweise Dies.
inf3rno
10

Es scheint, dass Tests im Grunde dasselbe wie der Code ausdrücken und daher ein Duplikat sind

Nein, das ist nicht wahr.

Tests haben einen anderen Zweck als Ihre Implementierung:

  • Tests stellen sicher, dass Ihre Implementierung funktioniert.
  • Sie dienen als Dokumentation: Wenn Sie sich die Tests ansehen, sehen Sie die Verträge, die Ihr Code erfüllen muss, dh welche Eingabe welche Ausgabe zurückgibt, was sind die Sonderfälle usw.
  • Ihre Tests garantieren außerdem, dass beim Hinzufügen neuer Funktionen Ihre vorhandenen Funktionen nicht beeinträchtigt werden.
Uooo
quelle
4

Nein. Bei DRY geht es darum, Code nur einmal zu schreiben, um eine bestimmte Aufgabe auszuführen. Test ist die Bestätigung, dass die Aufgabe korrekt ausgeführt wird. Es ist ein bisschen wie ein Abstimmungsalgorithmus, bei dem die Verwendung des gleichen Codes offensichtlich nutzlos wäre.

jmoreno
quelle
2

Würde das ultimative Ziel von DRY nicht die Eliminierung des gesamten Testcodes beinhalten?

Nein, das ultimative Ziel von DRY würde tatsächlich die Eliminierung des gesamten Produktionscodes bedeuten .

Wenn unsere Tests perfekte Spezifikationen für das System sein könnten, müssten wir nur automatisch den entsprechenden Produktionscode (oder die entsprechenden Binärdateien) generieren und so die Produktionscodebasis per se effektiv entfernen.

Dies ist tatsächlich das, was Ansätze wie die modellgetriebene Architektur zu erreichen behaupten - eine einzige vom Menschen entworfene Quelle der Wahrheit, aus der alles durch Berechnung abgeleitet wird.

Ich denke nicht, dass das Gegenteil (alle Tests loswerden) wünschenswert ist, weil:

  • Sie müssen die Impedanzfehlanpassung zwischen Implementierung und Spezifikation beheben. Produktionscode kann bis zu einem gewissen Grad Absichten vermitteln, aber es wird nie so einfach sein, über gut formulierte Tests nachzudenken. Wir Menschen brauchen diese höhere Sicht darauf, warum wir Dinge bauen. Selbst wenn Sie wegen DRY keine Tests durchführen, müssen die Spezifikationen wahrscheinlich ohnehin in Dokumenten festgehalten werden, was in Bezug auf Impedanzfehlanpassung und Code-Desynchronisation definitiv gefährlicher ist, wenn Sie mich fragen.
  • Während Produktionscode wahrscheinlich leicht aus korrekten ausführbaren Spezifikationen abgeleitet werden kann (vorausgesetzt, es wird genügend Zeit benötigt), ist es viel schwieriger, eine Testsuite aus dem endgültigen Code eines Programms wiederherzustellen. Die Spezifikationen werden nicht nur beim Betrachten des Codes klar angezeigt, da Interaktionen zwischen Codeeinheiten zur Laufzeit schwer zu erkennen sind. Aus diesem Grund fällt es uns so schwer, mit testlosen Legacy-Anwendungen umzugehen. Mit anderen Worten: Wenn Sie möchten, dass Ihre Anwendung länger als ein paar Monate überlebt, ist es wahrscheinlich besser, die Festplatte zu verlieren, auf der sich Ihre Produktionscodebasis befindet, als die, auf der sich Ihre Testsuite befindet.
  • Es ist viel einfacher, versehentlich einen Fehler in den Produktionscode einzuführen als in den Testcode. Und da sich der Produktionscode nicht selbst überprüft (obwohl dies mit Design by Contract- oder umfangreicheren Systemen möglich ist), benötigen wir noch ein externes Programm, um ihn zu testen und uns zu warnen, wenn eine Regression auftritt.
guillaume31
quelle
1

Weil es manchmal in Ordnung ist, sich zu wiederholen. Keines dieser Prinzipien soll unter allen Umständen ohne Frage oder Kontext verstanden werden. Ich habe manchmal Tests gegen eine naive (und langsame) Version eines Algorithmus geschrieben, was eine ziemlich eindeutige Verletzung von DRY darstellt, aber definitiv von Vorteil ist.

U2EF1
quelle
1

Da es beim Unit-Test darum geht, unbeabsichtigte Änderungen zu erschweren, können manchmal auch absichtliche Änderungen erschwert werden . Diese Tatsache hängt in der Tat mit dem DRY-Prinzip zusammen.

Wenn Sie beispielsweise eine Funktion haben, MyFunctiondie im Produktionscode nur an einer Stelle aufgerufen wird, und 20 Komponententests dafür schreiben, können Sie leicht 21 Stellen in Ihrem Code haben, an denen diese Funktion aufgerufen wird. Wenn Sie nun die Signatur MyFunctionoder die Semantik oder beides ändern müssen (da sich einige Anforderungen ändern), müssen Sie 21 Stellen anstelle von nur einer ändern. Und der Grund ist in der Tat ein Verstoß gegen das DRY-Prinzip: Sie haben (mindestens) den gleichen Funktionsaufruf MyFunction21 Mal wiederholt .

Der richtige Ansatz für einen solchen Fall besteht darin, das DRY-Prinzip auch auf Ihren Testcode anzuwenden: Wenn Sie 20 Komponententests schreiben, kapseln Sie die Aufrufe MyFunctionin Ihren Komponententests in nur wenigen Hilfsfunktionen (im Idealfall nur einer), die von der 20 Unit-Tests. Im Idealfall haben Sie nur zwei Stellen in Ihrem Code-Aufruf MyFunction: eine aus Ihrem Produktionscode und eine aus Ihren Unit-Tests. Wenn Sie also die Signatur von MyFunctionspäter ändern müssen , haben Sie nur wenige Stellen, an denen Sie Ihre Tests ändern können.

"Ein paar Stellen" sind immer noch mehr als "ein Ort" (was Sie ohne Unit-Tests erhalten), aber die Vorteile von Unit-Tests sollten den Vorteil, weniger Code zu ändern, stark überwiegen (andernfalls führen Sie Unit-Tests vollständig durch falsch).

Doc Brown
quelle
0

Eine der größten Herausforderungen beim Erstellen von Software besteht darin, die Anforderungen zu erfassen. das heißt, um die Frage zu beantworten: "Was soll diese Software tun?" Software benötigt genaue Anforderungen, um genau zu definieren, was das System tun muss. Zu denjenigen, die die Anforderungen für Softwaresysteme und -projekte definieren, gehören jedoch häufig Personen ohne Software oder formalen (mathematischen) Hintergrund. Die mangelnde Genauigkeit bei der Definition von Anforderungen zwang die Softwareentwicklung dazu, einen Weg zu finden, um Software an die Anforderungen anzupassen.

Das Entwicklungsteam stellte fest, dass die umgangssprachliche Beschreibung für ein Projekt in strengere Anforderungen übersetzt wurde. Die Testdisziplin hat sich als Kontrollpunkt für die Softwareentwicklung zusammengeschlossen, um die Lücke zwischen dem, was ein Kunde wünscht, und dem, was Software will, zu schließen. Sowohl die Softwareentwickler als auch das Qualitäts- / Testteam bilden ein Verständnis für die (informelle) Spezifikation und schreiben (unabhängig) Software oder Tests, um sicherzustellen, dass ihr Verständnis übereinstimmt. Durch Hinzufügen einer weiteren Person zum Verständnis der (ungenauen) Anforderungen wurden Fragen und eine andere Perspektive hinzugefügt, um die Genauigkeit der Anforderungen weiter zu verbessern.

Da es immer Abnahmetests gegeben hat, war es selbstverständlich, die Testrolle zu erweitern, um automatisierte Tests und Komponententests zu schreiben. Das Problem bestand darin, dass Programmierer eingestellt wurden, um Tests durchzuführen, und Sie die Perspektive von der Qualitätssicherung auf Programmierer, die Tests durchführen, einschränkten.

Trotzdem machen Sie wahrscheinlich falsche Tests, wenn sich Ihre Tests kaum von den tatsächlichen Programmen unterscheiden. Msdys Vorschlag wäre, sich mehr auf das zu konzentrieren, was in den Tests und weniger auf das Wie.

Die Ironie ist, dass die Industrie, anstatt eine formale Spezifikation der Anforderungen aus der umgangssprachlichen Beschreibung zu erfassen, Punkttests als Code zur Automatisierung des Testens implementiert hat. Anstatt formale Anforderungen zu erstellen, für deren Beantwortung Software erstellt werden könnte, bestand der Ansatz darin, einige Punkte zu testen, anstatt Software mithilfe formaler Logik zu erstellen. Dies ist ein Kompromiss, der jedoch ziemlich effektiv und relativ erfolgreich war.

ChuckCottrill
quelle
0

Wenn Sie der Meinung sind, dass Ihr Testcode Ihrem Implementierungscode zu ähnlich ist, kann dies ein Hinweis darauf sein, dass Sie ein Mocking-Framework zu häufig verwenden. Mock-basierte Tests auf einem zu niedrigen Niveau können dazu führen, dass der Testaufbau der getesteten Methode sehr ähnlich sieht. Versuchen Sie, Tests auf höherer Ebene zu schreiben, bei denen die Wahrscheinlichkeit einer Unterbrechung geringer ist, wenn Sie Ihre Implementierung ändern (ich weiß, dass dies schwierig sein kann, aber wenn Sie es verwalten können, erhalten Sie eine nützlichere Testsuite).

Jules
quelle
0

Unit-Tests sollten keine Duplizierung des zu testenden Codes enthalten, wie bereits erwähnt.

Ich würde jedoch hinzufügen, dass Unit-Tests normalerweise nicht so trocken sind wie "Produktions" -Code, da das Setup in allen Tests ähnlich (aber nicht identisch) ist ... insbesondere, wenn Sie eine erhebliche Anzahl von Abhängigkeiten haben, die Sie verspotten /Fälschung.
Es ist natürlich möglich, solche Dinge in eine übliche Setup-Methode (oder eine Reihe von Setup-Methoden) umzuwandeln ... aber ich habe festgestellt, dass diese Setup-Methoden dazu neigen, lange Parameterlisten zu haben und ziemlich spröde zu sein.

Sei also pragmatisch. Wenn Sie Setup-Code konsolidieren können, ohne die Wartbarkeit zu beeinträchtigen, tun Sie dies auf jeden Fall. Wenn die Alternative jedoch ein komplexer und spröder Satz von Einrichtungsmethoden ist, ist ein wenig Wiederholung in Ihren Testmethoden in Ordnung.

Ein lokaler TDD / BDD-Evangelist drückt es so aus:
"Ihr Produktionscode sollte TROCKEN sein. Aber es ist in Ordnung, wenn Ihre Tests 'feucht' sind."

David
quelle
0

Es scheint, dass Tests im Grunde dasselbe wie der Code ausdrücken und daher ein Duplikat (im Konzept, nicht in der Implementierung) des Codes sind.

Dies ist nicht wahr, Tests beschreiben die Anwendungsfälle, während der Code einen Algorithmus beschreibt, der die Anwendungsfälle besteht, also allgemeiner. Mit TDD beginnen Sie mit dem Schreiben von Anwendungsfällen (wahrscheinlich basierend auf der User Story) und implementieren anschließend den Code, der zum Übergeben dieser Anwendungsfälle erforderlich ist. Sie schreiben also einen kleinen Test, einen kleinen Teil des Codes, und überarbeiten danach gegebenenfalls, um die Wiederholungen zu beseitigen. So funktioniert das.

Durch Tests kann es auch zu Wiederholungen kommen. Zum Beispiel können Sie Geräte, Code zum Generieren von Geräten, komplizierte Behauptungen usw. wiederverwenden. Normalerweise mache ich das, um Fehler in den Tests zu vermeiden, aber ich vergesse normalerweise, zuerst zu testen, ob ein Test wirklich fehlschlägt, und es kann den Tag wirklich ruinieren , wenn Sie eine halbe Stunde lang nach dem Fehler im Code suchen und der Test falsch ist ... xD

inf3rno
quelle