Entwerfen von Komponententests für ein statusbehaftetes System


20

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?


2
Ich glaube nicht, dass Sie eine elegante Lösung dafür finden werden. Der allgemeine Ansatz besteht nicht darin, das System zunächst in einen statusabhängigen Zustand zu versetzen, was beim Testen von bereits erstellten Elementen nicht hilfreich ist. Das Umgestalten, um staatenlos zu sein, ist wahrscheinlich auch die Kosten nicht wert.
Doval


@Doval: Bitte erläutern Sie, wie Sie so etwas wie ein Telefon (SIP UserAgent) nicht zustandsfähig machen. Das erwartete Verhalten dieser Unit wird im RFC anhand eines Zustandsübergangsdiagramms festgelegt.
Bart van Ingen Schenau

Kopieren Sie Ihre Tests, fügen Sie sie ein, bearbeiten Sie sie, oder schreiben Sie Dienstprogrammmethoden, um gemeinsame Einstellungen / Abbrüche / Funktionen zu nutzen? Während einige Testfälle sicherlich lang und aufgebläht werden können, sollte dies nicht allzu häufig sein. In einem statusbehafteten System würde ich eine übliche Setup-Routine erwarten, bei der der Endstatus ein Parameter ist und Sie mit dieser Routine in den Status versetzt werden, den Sie testen möchten. Zusätzlich würde ich am Ende jedes Tests eine Auflösungsmethode haben, die Sie in den bekannten Startzustand zurückbringt (falls dies erforderlich ist), damit Ihre Einrichtungsmethode ordnungsgemäß funktioniert, wenn der nächste Test beginnt.
Dunk

Zu einer Tangente möchte ich jedoch hinzufügen, dass Zustandsdiagramme ein Kommunikationswerkzeug und kein Implementierungsdekret sind, selbst wenn es sich um einen RFC handelt. Solange Sie die beschriebene Funktionalität erfüllen, erfüllen Sie den Standard. In einigen Fällen habe ich wirklich komplizierte Implementierungen von Statusübergängen (wie in RFCs definiert) in wirklich einfache allgemeine Verarbeitungsfunktionen konvertiert. Ein Fall, an den ich mich erinnere, als ich ein paar tausend Codezeilen loswurde, als mir klar wurde, dass andere als ein paar Flags mit etwa fünf Zuständen genau dasselbe taten, als Sie die "verborgenen" gemeinsamen Elemente umbenannten.
Eintauchen

Antworten:


15

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 foosowie ein anderer Wert in barangezeigt werden, und kann dies foo != barals 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 einem Initund -Zustand basieren State1. Sie möchten Ihren ChangeFooToState1Test als Einrichtungsschritt verwenden, um die Ausgabe zu testen, wenn "eingeschaltet" Foo()ist State1.

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 ChangeFooToState1zwei Ziele definieren, entsprechen Sie immer noch dem Geist dessen, was SAP uns beibringt. Wenn Sie sicherstellen müssen, dass sich der Status Foo()ändert, verwenden Sie dies ChangeFooToState1als Test. Und wenn Sie die Foo()Ausgabe von State1" validieren müssen, wenn ", dann verwenden Sie ChangeFooToState1als 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 writeTests 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.


11

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.


Nur zur Veranschaulichung, das SIP-Beispiel war meins, nicht vom OP. Und einige Zustandsautomaten benötigen möglicherweise mehr als ein paar Methodenaufrufe, um sie für einen bestimmten Test in den richtigen Zustand zu versetzen.
Bart van Ingen Schenau

+1 für "Sie können Zustand nicht ganz vermeiden, aber Sie können ihn minimieren und isolieren." Ich konnte nicht zustimmen. Staat ist ein notwendiges Übel in der Software.
Brandon

0

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:

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.