Hintergrund
Test Driven Development wurde populär, nachdem ich bereits die Schule abgeschlossen hatte und in der Industrie war. Ich versuche es zu lernen, aber einige wichtige Dinge entgehen mir immer noch. TDD-Befürworter sagen viele Dinge wie (im Folgenden als "Single-Assertion-Prinzip" oder SAP bezeichnet ):
Seit einiger Zeit habe ich darüber nachgedacht, wie TDD-Tests so einfach, aussagekräftig und elegant wie möglich sein können. In diesem Artikel wird ein wenig untersucht, wie es ist, Tests so einfach und zerlegt wie möglich zu gestalten: Ziel ist es, in jedem Test eine einzige Aussage zu treffen.
Quelle: http://www.artima.com/weblogs/viewpost.jsp?thread=35578
Sie sagen auch solche Dinge (im Folgenden als "Prinzip der privaten Methode" oder PMP bezeichnet ):
Private Methoden werden in der Regel nicht direkt in einem Komponententest getestet. Betrachten Sie sie als Implementierungsdetail, da sie privat sind. Niemand wird jemals einen von ihnen anrufen und erwarten, dass es auf eine bestimmte Art und Weise funktioniert.
Sie sollten stattdessen Ihre öffentliche Schnittstelle testen. Wenn die Methoden, die Ihre privaten Methoden aufrufen, erwartungsgemäß funktionieren, gehen Sie davon aus, dass Ihre privaten Methoden ordnungsgemäß funktionieren.
Quelle: Wie testest du private Methoden?
Lage
Ich versuche ein zustandsbehaftetes Datenverarbeitungssystem zu testen. Das System kann verschiedene Aktionen für genau die gleichen Daten ausführen, vorausgesetzt, der Status war vor dem Empfang dieser Daten. Stellen Sie sich einen einfachen Test vor, der den Status im System aufbaut, und testen Sie dann das Verhalten, das mit der angegebenen Methode getestet werden soll.
SAP schlägt vor, dass ich die "Statusaufbauprozedur" nicht testen soll. Ich sollte davon ausgehen, dass der Status dem entspricht, den ich vom Build-Code erwarte, und dann die einzige Statusänderung testen, die ich testen möchte
PMP schlägt vor, dass ich diesen Schritt zum Aufbau des Zustands nicht überspringen und nur die Methoden testen kann, die diese Funktionalität unabhängig voneinander steuern.
Das Ergebnis in meinem eigentlichen Code waren Tests, die aufgebläht, kompliziert, lang und schwer zu schreiben sind. Und wenn sich die Zustandsübergänge ändern, müssen die Tests geändert werden ... was für kleine, effiziente Tests in Ordnung wäre, aber extrem zeitaufwendig und verwirrend bei diesen langen, aufgeblähten Tests. Wie wird das normalerweise gemacht?
quelle
Antworten:
Perspektive:
Machen wir also einen Schritt zurück und fragen, bei welchen Themen TDD uns helfen möchte. TDD versucht uns zu helfen, festzustellen, ob unser Code korrekt ist oder nicht. Und mit richtig meine ich: "Entspricht der Code den Geschäftsanforderungen?" Das Verkaufsargument ist, dass wir wissen, dass in Zukunft Änderungen erforderlich sind, und wir möchten sicherstellen, dass unser Code auch nach diesen Änderungen korrekt bleibt.
Ich spreche diese Perspektive an, weil ich denke, dass es leicht ist, sich in den Details zu verlieren und aus den Augen zu verlieren, was wir erreichen wollen.
Grundsätze - SAP:
Ich bin zwar kein TDD-Experte, aber ich denke, Sie vermissen einen Teil dessen, was das Single Assertion Principle (SAP) zu lehren versucht. SAP kann wie folgt umformuliert werden: "Test eins nach dem anderen". Aber TOTAT lässt sich nicht so leicht von der Zunge rollen wie SAP.
Wenn Sie immer nur eine Sache testen, konzentrieren Sie sich auf einen Fall. ein Weg; eine Randbedingung; ein Fehlerfall; eine was auch immer pro Test. Und die treibende Idee dahinter ist, dass Sie wissen müssen, was kaputt gegangen ist, als der Testfall fehlgeschlagen ist, damit Sie das Problem schneller lösen können. Wenn Sie mehrere Bedingungen (dh mehr als eine Sache) innerhalb eines Tests testen und der Test fehlschlägt, haben Sie viel mehr Arbeit an Ihren Händen. Sie müssen zuerst herausfinden, welcher der mehreren Fälle fehlgeschlagen ist, und dann herausfinden, warum dieser Fall fehlgeschlagen ist.
Wenn Sie eine Sache nach der anderen testen, ist Ihr Suchbereich viel kleiner und der Fehler wird schneller identifiziert. Denken Sie daran, dass "eine Sache nach der anderen testen" Sie nicht unbedingt davon ausschließt, mehr als eine Prozessausgabe gleichzeitig zu betrachten. Wenn ich beispielsweise einen "bekannten guten Pfad" teste, erwarte ich möglicherweise, dass ein bestimmter, resultierender Wert
foo
sowie ein anderer Wert inbar
angezeigt werden, und kann diesfoo != bar
als Teil meines Tests überprüfen. Der Schlüssel besteht darin, die Ausgabeprüfungen basierend auf dem getesteten Fall logisch zu gruppieren.Grundsätze - PMP:
Ebenso vermisse ich ein bisschen, was das Private Method Principle (PMP) uns beibringen muss. PMP ermutigt uns, das System wie eine Black Box zu behandeln. Für eine bestimmte Eingabe sollten Sie eine bestimmte Ausgabe erhalten. Es ist Ihnen egal, wie die Blackbox die Ausgabe generiert. Sie kümmern sich nur darum, dass Ihre Ausgaben mit Ihren Eingaben übereinstimmen.
PMP ist eine wirklich gute Perspektive, um die API-Aspekte Ihres Codes zu betrachten. Es kann Ihnen auch dabei helfen, den Umfang der zu testenden Elemente zu bestimmen. Identifizieren Sie Ihre Schnittstellen und stellen Sie sicher, dass sie die Bedingungen ihrer Verträge erfüllen. Sie müssen sich nicht darum kümmern, wie die (auch als privat bezeichneten) Methoden hinter der Benutzeroberfläche ihre Arbeit erledigen. Sie müssen nur überprüfen, ob sie das getan haben, was sie tun sollten.
Angewandte TDD ( für Sie )
Ihre Situation ist also etwas faltiger als bei einer normalen Anwendung. Die Methoden Ihrer App sind statusbehaftet, sodass ihre Ausgabe nicht nur von der Eingabe, sondern auch von den zuvor ausgeführten Aktionen abhängt. Ich bin sicher, ich sollte
<insert some lecture>
hier über den Staat sprechen, der schrecklich ist und bla bla bla, aber das hilft wirklich nicht, dein Problem zu lösen.Ich gehe davon aus, dass Sie eine Art Zustandsdiagramm-Tabelle haben, in der die verschiedenen möglichen Zustände und die erforderlichen Schritte zum Auslösen eines Übergangs aufgeführt sind. Wenn Sie dies nicht tun, benötigen Sie es, da es die Geschäftsanforderungen für dieses System ausdrückt.
Die Tests: Zuerst müssen Sie eine Reihe von Tests durchführen, mit denen sich der Status ändert. Im Idealfall stehen Tests zur Verfügung, die alle möglichen Statusänderungen durchführen. Ich kann jedoch einige Szenarien vorstellen, in denen Sie möglicherweise nicht in vollem Umfang vorgehen müssen.
Als Nächstes müssen Sie Tests erstellen, um die Datenverarbeitung zu validieren. Einige dieser Zustandstests werden beim Erstellen der Datenverarbeitungstests wiederverwendet. Angenommen, Sie haben eine Methode
Foo()
mit unterschiedlichen Ausgaben, die auf einemInit
und -Zustand basierenState1
. Sie möchten IhrenChangeFooToState1
Test als Einrichtungsschritt verwenden, um die Ausgabe zu testen, wenn "eingeschaltet"Foo()
istState1
.Es gibt einige Implikationen hinter diesem Ansatz, die ich erwähnen möchte. Spoiler, hier werde ich die Puristen verärgern
Zunächst müssen Sie akzeptieren, dass Sie in einer Situation etwas als Test und in einer anderen Situation ein Setup verwenden. Einerseits scheint dies eine direkte Verletzung von SAP zu sein. Wenn Sie jedoch logischerweise
ChangeFooToState1
zwei Ziele definieren, entsprechen Sie immer noch dem Geist dessen, was SAP uns beibringt. Wenn Sie sicherstellen müssen, dass sich der StatusFoo()
ändert, verwenden Sie diesChangeFooToState1
als Test. Und wenn Sie dieFoo()
Ausgabe vonState1
" validieren müssen, wenn ", dann verwenden SieChangeFooToState1
als Setup.Der zweite Punkt ist, dass Sie aus praktischer Sicht keine vollständig randomisierten Komponententests für Ihr System wünschen. Sie sollten alle Statusänderungstests ausführen, bevor Sie die Ausgabevalidierungstests ausführen. SAP ist sozusagen der Leitgedanke hinter dieser Bestellung. Um festzustellen, was offensichtlich sein sollte - Sie können etwas nicht als Setup verwenden, wenn es als Test fehlschlägt.
Etwas zusammensetzen:
Mithilfe Ihres Zustandsdiagramms generieren Sie Tests, um die Übergänge abzudecken. Wiederum generieren Sie anhand Ihres Diagramms Tests, um alle vom Status abhängigen Fälle der Eingabe- / Ausgabedatenverarbeitung abzudecken.
Wenn Sie diesem Ansatz folgen, sollten die
bloated, complicated, long, and difficult to write
Tests etwas einfacher zu handhaben sein. Im Allgemeinen sollten sie kleiner und übersichtlicher (dh weniger kompliziert) sein. Sie sollten beachten, dass die Tests auch entkoppelt oder modular sind.Nun, ich sage nicht, dass der Prozess völlig schmerzfrei sein wird, da das Schreiben guter Tests einige Anstrengungen erfordert. Und einige von ihnen werden immer noch schwierig sein, weil Sie einen zweiten Parameter (Zustand) auf einige Ihrer Fälle abbilden. Abgesehen davon sollte es ein wenig offensichtlicher sein, warum es einfacher ist, Tests für ein zustandsloses System zu erstellen. Wenn Sie diesen Ansatz jedoch für Ihre Anwendung anpassen, sollten Sie feststellen, dass Sie nachweisen können, dass Ihre Anwendung ordnungsgemäß funktioniert.
quelle
Normalerweise abstrahieren Sie die Einrichtungsdetails in Funktionen, damit Sie sich nicht wiederholen müssen. Auf diese Weise müssen Sie es nur an einer Stelle im Test ändern, wenn sich die Funktionalität ändert.
Normalerweise möchten Sie jedoch nicht einmal Ihre Setup-Funktionen als aufgebläht, kompliziert oder lang beschreiben. Dies ist ein Zeichen dafür, dass Ihre Benutzeroberfläche überarbeitet werden muss. Wenn es für Ihre Tests schwierig ist, sie zu verwenden, ist es auch für Ihren echten Code schwierig.
Das ist oft ein Zeichen dafür, dass man zu viel in eine Klasse steckt. Wenn Sie Zustandsanforderungen haben, benötigen Sie eine Klasse, die den Zustand verwaltet, und sonst nichts. Die Klassen, die es unterstützen, sollten zustandslos sein. Für Ihr SIP-Beispiel sollte das Parsen eines Pakets vollständig zustandslos sein. Sie können eine Klasse haben, die ein Paket analysiert und dann etwas aufruft
sipStateController.receiveInvite()
, um die Zustandsübergänge zu verwalten. Diese Klasse ruft selbst andere zustandslose Klassen auf, um Dinge wie das Klingeln des Telefons zu tun.Dies macht das Einrichten des Unit-Tests für die State-Machine-Klasse zu einer einfachen Angelegenheit von wenigen Methodenaufrufen. Wenn Ihr Setup für State-Machine-Unit-Tests das Erstellen von Paketen erfordert, haben Sie zu viel in diese Klasse gesteckt. Ebenso sollte Ihre Paket-Parser-Klasse relativ einfach zu erstellen sein und einen Mock für die State-Machine-Klasse verwenden.
Mit anderen Worten, Sie können den Status nicht vollständig vermeiden, ihn jedoch minimieren und isolieren.
quelle
Die Grundidee von TDD ist, dass Sie, wenn Sie zuerst Tests schreiben, ein System erhalten, das zumindest einfach zu testen ist. Hoffentlich funktioniert es, ist wartbar, gut dokumentiert und so weiter, aber wenn nicht, ist es zumindest immer noch einfach zu testen.
Wenn Sie also eine TDD durchführen und ein System haben, das nur schwer zu testen ist, ist ein Fehler aufgetreten. Vielleicht sollten einige Dinge, die privat sind, öffentlich sein, weil Sie sie zum Testen benötigen. Vielleicht arbeiten Sie nicht auf der richtigen Abstraktionsebene. etwas so Einfaches wie eine Liste ist auf einer Ebene statusbehaftet, auf einer anderen jedoch ein Wert. Oder Sie geben Ratschlägen, die in Ihrem Kontext nicht zutreffen, zu viel Gewicht, oder Ihr Problem ist nur schwer. Oder natürlich, vielleicht ist Ihr Design einfach schlecht.
Was auch immer die Ursache sein mag, Sie werden wahrscheinlich nicht zurückkehren und Ihr System erneut schreiben, um es mit einfachem Testcode testbarer zu machen. So wahrscheinlich ist der beste Plan, einige etwas ausgefallenere Testtechniken zu verwenden, wie:
quelle