Kampf mit zyklischen Abhängigkeiten in Unit-Tests

24

Ich versuche, TDD zu üben, indem ich es verwende, um einen einfachen Bit-Vektor zu entwickeln. Ich benutze Swift, aber das ist eine sprachunabhängige Frage.

My BitVectorist ein structSpeicherort für eine einzelne UInt64Datei und bietet darüber eine API, mit der Sie sie wie eine Sammlung behandeln können. Die Details machen nicht viel aus, aber es ist ziemlich einfach. Die hohen 57 Bits sind Speicherbits und die unteren 6 Bits sind "Zählbits", die Ihnen mitteilen, wie viele der Speicherbits tatsächlich einen enthaltenen Wert speichern.

Bisher habe ich eine Handvoll sehr einfacher Fähigkeiten:

  1. Ein Initialisierer, der leere Bitvektoren erstellt
  2. Eine countEigenschaft vom TypInt
  3. Eine isEmptyEigenschaft vom TypBool
  4. Ein Gleichheitsoperator ( ==). NB: Dies ist ein Object.equals()in Java ähnlicher Wertegleichheitsoperator und kein Referenzgleichheitsoperator wie ==in Java.

Ich bin mit einer Reihe von zyklischen Abhängigkeiten konfrontiert:

  1. Der Komponententest, der meinen Initialisierer testet, muss überprüfen, ob er neu erstellt wurde BitVector. Dies kann auf drei Arten geschehen:

    1. Prüfen bv.count == 0
    2. Prüfen bv.isEmpty == true
    3. Prüfe das bv == knownEmptyBitVector

    Methode 1 stützt sich auf countMethode 2 stützt sich auf isEmpty(die sich selbst stützt count, es macht also keinen Sinn, sie zu verwenden), Methode 3 stützt sich auf ==. In jedem Fall kann ich meinen Initialisierer nicht isoliert testen.

  2. Der Test für countmuss auf etwas funktionieren, was unweigerlich meine Initialisierer testet

  3. Die Umsetzung von isEmptysetzt aufcount

  4. Die Umsetzung von ==setzt auf count.

Ich konnte dieses Problem teilweise lösen, indem ich eine private API einführte, die BitVectoraus einem vorhandenen Bitmuster (als a UInt64) ein erstellt. Dies ermöglichte es mir, Werte zu initialisieren, ohne andere Initialisierer zu testen, so dass ich meinen Weg nach oben "anschnallen" konnte.

Damit meine Komponententests wirklich Komponententests sind, mache ich eine Reihe von Hacks, die meinen Produkt- und Testcode erheblich komplizieren.

Wie genau gehen Sie mit solchen Problemen um?

Alexander - Setzen Sie Monica wieder ein
quelle
20
Sie sehen den Begriff "Einheit" zu eng. BitVectorist eine perfekte Größe für Unit-Tests und behebt sofort Ihre Probleme, die öffentliche Mitglieder BitVectorgegenseitig benötigen, um aussagekräftige Tests durchzuführen.
Bart van Ingen Schenau
Sie kennen im Vorfeld zu viele Implementierungsdetails. Ist Ihre Entwicklung wirklich testgetrieben ?
Herby
@herby Nein, deshalb übe ich. Obwohl das ein wirklich unerreichbarer Standard zu sein scheint. Ich glaube nicht, dass ich jemals etwas programmiert habe, ohne eine ziemlich klare Vorstellung davon zu haben, was die Implementierung mit sich bringen wird.
Alexander - Reinstate Monica
@Alexander Du solltest versuchen, dich zu entspannen, sonst wird es zuerst getestet, aber nicht getestet. Sagen Sie einfach vage "Ich mache einen Bit-Vektor mit einem 64-Bit-Int als Hintergrundspeicher" und das war's. von diesem Punkt an TDD Rot-Grün-Refaktor nacheinander tun. Implementierungsdetails sowie die API sollten sich aus dem Versuch ergeben, Tests ausführen zu lassen (erstere), und aus dem Schreiben dieser Tests an erster Stelle (letztere).
Herby

Antworten:

66

Sie machen sich zu viele Gedanken über Implementierungsdetails.

Es spielt keine Rolle , dass in der aktuellen Implementierung , isEmptystützt sich auf count(oder was auch immer andere Beziehungen könnten Sie haben): alles , was Sie über die Pflege werden sollen , ist die öffentliche Schnittstelle. Sie können beispielsweise drei Tests durchführen:

  • Das hat ein neu initialisiertes Objekt count == 0.
  • Das hat ein neu initialisiertes Objekt isEmpty == true
  • Dass ein neu initialisiertes Objekt gleich dem bekannten leeren Objekt ist.

Dies sind alles gültige Tests und werden besonders wichtig, wenn Sie sich jemals dazu entschließen, die Interna Ihrer Klasse umzugestalten, isEmptydamit eine andere Implementierung verwendet wird, die sich nicht darauf stützt. countSolange Ihre Tests alle bestehen, wissen Sie, dass Sie nicht zurückgegangen sind etwas.

Ähnliches gilt für Ihre anderen Punkte - denken Sie daran, die öffentliche Schnittstelle und nicht Ihre interne Implementierung zu testen. Sie können TDD hier nützlich finden, da Sie dann die Tests schreiben, die Sie benötigen, isEmptybevor Sie überhaupt eine Implementierung dafür geschrieben haben.

Philip Kendall
quelle
6
@Alexander Sie klingen wie ein Mann, der eine klare Definition für Unit-Tests benötigt. Das beste, das ich kenne, kommt von Michael Feathers
candied_orange
14
@Alexander Sie behandeln jede Methode als unabhängig testbaren Code. Das ist die Quelle Ihrer Schwierigkeiten. Diese Schwierigkeiten verschwinden, wenn Sie das Objekt als Ganzes testen, ohne zu versuchen, es in kleinere Teile zu unterteilen. Abhängigkeiten zwischen Objekten sind nicht mit Abhängigkeiten zwischen Methoden vergleichbar.
amon
9
@Alexander "ein Stück Code" ist eine willkürliche Messung. Durch das Initialisieren einer Variablen verwenden Sie viele "Codeteile". Entscheidend ist, dass Sie eine von Ihnen definierte zusammenhängende Verhaltenseinheit testen .
Ant P
9
"Nach allem, was ich gelesen habe, habe ich den Eindruck, dass nur Unit-Tests, die sich direkt auf diesen Code beziehen, scheitern sollten, wenn Sie nur einen Teil des Codes brechen." Das scheint eine sehr schwierige Regel zu sein. (Wenn Sie z. B. eine Vektorklasse schreiben und einen Fehler in der Indexmethode machen, wird der gesamte Code, der diese
Vektorklasse
4
@Alexander Sehen Sie sich auch das Muster "Anordnen, Act, Assert" für Tests an. Grundsätzlich richten Sie das Objekt in dem Zustand ein, in dem es sich befinden muss (Anordnen), rufen die Methode auf, die Sie tatsächlich testen (Act), und überprüfen dann, ob sich der Zustand gemäß Ihren Erwartungen geändert hat. (Behaupten). Sachen, die Sie in Arrangieren eingerichtet haben, wären "Voraussetzungen" für den Test.
GalacticCowboy
5

Wie genau gehen Sie mit solchen Problemen um?

Sie überdenken, was ein "Komponententest" ist.

Ein Objekt, das veränderbare Daten im Speicher verwaltet, ist im Grunde eine Zustandsmaschine. Jeder wertvolle Anwendungsfall ruft also mindestens eine Methode zum Einfügen von Informationen in das Objekt und eine Methode zum Auslesen einer Kopie von Informationen aus dem Objekt auf. In den interessanten Anwendungsfällen werden Sie auch zusätzliche Methoden aufrufen, die die Datenstruktur ändern.

In der Praxis sieht das oft so aus

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

oder

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

Die "Unit-Test" -Terminologie - nun, es hat eine lange Geschichte, nicht sehr gut zu sein.

Ich nenne sie Unit-Tests, aber sie stimmen nicht sehr gut mit der akzeptierten Definition von Unit-Tests überein - Kent Beck, Test Driven Development by Example

Kent schrieb die erste Version von SUnit 1994 , die Portierung auf JUnit erfolgte 1998, der erste Entwurf des TDD-Buches war Anfang 2002. Die Verwirrung hatte viel Zeit, sich auszubreiten.

Die Schlüsselidee dieser Tests (genauer gesagt "Programmierertests" oder "Entwicklertests") ist, dass die Tests voneinander isoliert sind. Die Tests haben keine veränderlichen Datenstrukturen gemeinsam, sodass sie gleichzeitig ausgeführt werden können. Es ist nicht zu befürchten, dass die Tests in einer bestimmten Reihenfolge ausgeführt werden müssen, um die Lösung korrekt zu messen.

Der primäre Anwendungsfall für diese Tests besteht darin, dass sie von der Programmiererin zwischen den Änderungen an ihrem eigenen Quellcode ausgeführt werden. Wenn Sie das Rot-Grün-Refaktor-Protokoll ausführen, zeigt ein unerwartetes ROT immer einen Fehler in Ihrer letzten Bearbeitung an. Sie machen diese Änderung rückgängig, stellen sicher, dass die Tests GRÜN sind, und versuchen es erneut. Es ist nicht sehr vorteilhaft, in ein Design zu investieren, bei dem jeder mögliche Fehler von nur einem Test erfasst wird.

Wenn eine Zusammenführung einen Fehler verursacht, ist es natürlich nicht mehr trivial, diesen Fehler zu finden. Sie können verschiedene Schritte ausführen, um sicherzustellen, dass Fehler leicht lokalisiert werden können. Sehen

VoiceOfUnreason
quelle
1

Im Allgemeinen (auch wenn Sie TDD nicht verwenden) sollten Sie sich bemühen, so viele Tests wie möglich zu schreiben, während Sie so tun, als wüssten Sie nicht, wie es implementiert wird.

Wenn Sie TDD tatsächlich durchführen, sollte dies bereits der Fall sein. Ihre Tests sind eine ausführbare Spezifikation des Programms.

Wie das Aufrufdiagramm unter den Tests aussieht, ist unerheblich, solange die Tests selbst sinnvoll und gut gepflegt sind.

Ich denke, Ihr Problem ist Ihr Verständnis von TDD.

Meiner Meinung nach besteht Ihr Problem darin, dass Sie Ihre TDD-Personas "mischen". Ihre "Test" -, "Code" - und "Refactor" -Personen arbeiten im Idealfall völlig unabhängig voneinander. Insbesondere sind Ihre Kodierungs- und Umgestaltungspersonen nicht verpflichtet, die Tests anders als grün zu machen / am Laufen zu halten.

Sicher, im Prinzip wäre es am besten, wenn alle Tests orthogonal und unabhängig voneinander wären. Aber das ist kein Anliegen Ihrer beiden anderen TDD-Persönlichkeiten, und es ist definitiv keine strenge oder sogar unbedingt realistische Anforderung an Ihre Tests. Grundsätzlich gilt: Wirf nicht deinen gesunden Menschenverstand in Bezug auf die Codequalität aus, um zu versuchen, eine Anforderung zu erfüllen, die niemand von dir verlangt.

Tim Seguine
quelle