Sollte es "Arrange-Assert-Act-Assert" sein?


94

In Bezug auf das klassische Testmuster von Arrange-Act-Assert füge ich häufig eine Gegenbehauptung hinzu, die Act vorausgeht. Auf diese Weise weiß ich, dass die vorübergehende Behauptung als Ergebnis der Handlung wirklich vorübergeht.

Ich halte es für analog zum Rot im Rot-Grün-Refaktor. Nur wenn ich den roten Balken während meiner Tests gesehen habe, weiß ich, dass der grüne Balken bedeutet, dass ich Code geschrieben habe, der einen Unterschied macht. Wenn ich einen bestandenen Test schreibe, wird jeder Code ihn erfüllen. In ähnlicher Weise weiß ich in Bezug auf Arrange-Assert-Act-Assert, wenn meine erste Behauptung fehlschlägt, dass ein Gesetz die endgültige Behauptung bestanden hätte - so dass es tatsächlich nichts über das Gesetz verifiziert hat.

Folgen Ihre Tests diesem Muster? Warum oder warum nicht?

Update- Klarstellung: Die anfängliche Behauptung ist im Wesentlichen das Gegenteil der endgültigen Behauptung. Es ist keine Behauptung, dass Arrange funktioniert hat; Es ist eine Behauptung, dass Act noch nicht funktioniert hat.

Antworten:


121

Dies ist nicht die häufigste Vorgehensweise, aber immer noch häufig genug, um einen eigenen Namen zu haben. Diese Technik wird als Guard Assertion bezeichnet . Eine ausführliche Beschreibung finden Sie auf Seite 490 im hervorragenden Buch xUnit Test Patterns von Gerard Meszaros (sehr zu empfehlen).

Normalerweise verwende ich dieses Muster nicht selbst, da ich es für korrekter halte, einen bestimmten Test zu schreiben, der die Voraussetzungen bestätigt, die ich sicherstellen muss. Ein solcher Test sollte immer fehlschlagen, wenn die Vorbedingung fehlschlägt, und dies bedeutet, dass ich ihn nicht in alle anderen Tests eingebettet brauche. Dies bietet eine bessere Isolation von Bedenken, da ein Testfall nur eine Sache bestätigt.

Es kann viele Voraussetzungen geben, die für einen bestimmten Testfall erfüllt sein müssen, sodass Sie möglicherweise mehr als eine Guard Assertion benötigen. Anstatt diese in allen Tests zu wiederholen, hält ein (und nur ein) Test für jede Vorbedingung Ihren Testcode besser erhalten, da Sie auf diese Weise weniger Wiederholungen haben.


+1, sehr gute Antwort. Der letzte Teil ist besonders wichtig, da er zeigt, dass Sie die Dinge als separaten Komponententest schützen können.
Murrekatt

3
Ich habe es im Allgemeinen auch so gemacht, aber es gibt ein Problem mit einem separaten Test, um die Voraussetzungen sicherzustellen (insbesondere bei einer großen Codebasis mit sich ändernden Anforderungen) - der Vorbedingungstest wird im Laufe der Zeit geändert und nicht mehr mit dem Haupttest synchronisiert. Test, der diese Voraussetzungen voraussetzt. Die Voraussetzungen mögen also alle gut und grün sein, aber diese Voraussetzungen sind im Haupttest, der jetzt immer grün und gut zeigt, nicht erfüllt. Aber wenn die Voraussetzungen im Haupttest wären, wären sie gescheitert. Sind Sie auf dieses Problem gestoßen und haben eine gute Lösung dafür gefunden?
Nchaud

2
Wenn Sie Ihre Tests häufig ändern, können andere Probleme auftreten , da dies dazu führt, dass Ihre Tests weniger vertrauenswürdig sind. Erwägen Sie auch bei sich ändernden Anforderungen, Code nur als Anhang zu entwerfen .
Mark Seemann

@MarkSeemann Sie haben Recht, dass wir die Wiederholung minimieren müssen, aber auf der anderen Seite kann es viele Dinge geben, die sich auf die Anordnung für den spezifischen Test auswirken können, obwohl der Test für die Anordnung selbst durchlaufen würde. Zum Beispiel war die Bereinigung für den Anordnungs-Test oder nach einem anderen Test fehlerhaft und Anordnen wäre nicht dasselbe wie im Anordnen-Test.
Rekshino


8

Hier ist ein Beispiel.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Es könnte sein, dass ich geschrieben habe, Range.includes()um einfach true zurückzugeben. Ich habe es nicht getan, aber ich kann mir vorstellen, dass ich es haben könnte. Oder ich hätte es auf viele andere Arten falsch schreiben können. Ich würde hoffen und erwarten, dass ich mit TDD tatsächlich alles richtig gemacht habe - das includes()funktioniert einfach - aber vielleicht habe ich es nicht getan. Die erste Behauptung ist also eine Überprüfung der geistigen Gesundheit, um sicherzustellen, dass die zweite Behauptung wirklich aussagekräftig ist.

Von selbst gelesen assertTrue(range.includes(7));heißt es: "behaupten, dass der modifizierte Bereich 7 enthält". Im Zusammenhang mit der ersten Behauptung heißt es: "Behaupten Sie, dass das Aufrufen von umfassen () dazu führt, dass es 7 enthält. Und da umfassen die Einheit ist, die wir testen, denke ich, dass dies einen (kleinen) Wert hat.

Ich akzeptiere meine eigene Antwort. Viele der anderen haben meine Frage falsch verstanden, um das Setup zu testen. Ich denke das ist etwas anders.


Vielen Dank, dass Sie mit einem Beispiel zurückgekommen sind, Carl. Nun, im roten Teil des TDD-Zyklus, bis das umfassen () wirklich etwas tut; Die erste Behauptung ist sinnlos, es ist nur eine Verdoppelung der zweiten. Bei Grün fängt es an, nützlich zu sein. Beim Refactoring wird es sinnvoll. Es könnte schön sein, ein UT-Framework zu haben, das dies automatisch erledigt.
Philant

Angenommen, Sie TDD diese Range-Klasse, wird es nicht einen weiteren fehlgeschlagenen Test geben, der den Range-Ctor testet, wenn Sie ihn brechen?
Philant

1
@philippe: Ich bin nicht sicher, ob ich die Frage verstehe. Der Range-Konstruktor und include () haben ihre eigenen Unit-Tests. Könnten Sie bitte näher darauf eingehen?
Carl Manaster

Damit die erste AssertFalse-Assertion (range.includes (7)) fehlschlägt, muss der Range Constructor fehlerhaft sein. Ich wollte also fragen, ob die Tests für den Range-Konstruktor nicht gleichzeitig mit dieser Behauptung unterbrochen werden. Und was ist mit der Behauptung nach dem Gesetz auf einen anderen Wert: zB assertFalse (range.includes (6))?
Philant

1
Die Bereichskonstruktion steht meines Erachtens vor Funktionen wie include (). Obwohl ich damit einverstanden bin, würde nur ein fehlerhafter Konstruktor (oder ein fehlerhaftes Includes ()) dazu führen, dass diese erste Behauptung fehlschlägt. Der Test des Konstruktors würde jedoch keinen Aufruf von include () enthalten. Ja, alle Funktionen bis zur ersten Behauptung sind bereits getestet. Aber diese anfängliche negative Behauptung kommuniziert etwas und meiner Meinung nach etwas Nützliches. Selbst wenn jede solche Behauptung beim ersten Schreiben bestanden wird.
Carl Manaster

7

Ein Arrange-Assert-Act-AssertTest kann immer in zwei Tests umgestaltet werden:

1. Arrange-Assert

und

2. Arrange-Act-Assert

Der erste Test bestätigt nur das, was in der Arrangierphase eingerichtet wurde, und der zweite Test bestätigt nur das, was in der Act-Phase geschehen ist.

Dies hat den Vorteil, dass Sie ein genaueres Feedback darüber geben, ob die Arrangier- oder die Act-Phase fehlgeschlagen ist, während diese im Original Arrange-Assert-Act-Assertzusammengeführt werden und Sie tiefer graben und genau untersuchen müssten, welche Behauptung fehlgeschlagen ist und warum sie fehlgeschlagen ist, um festzustellen, ob Es war das Arrangieren oder Handeln, das fehlschlug.

Es erfüllt auch die Absicht, Unit-Tests besser durchzuführen, da Sie Ihren Test in kleinere unabhängige Einheiten aufteilen.

Denken Sie zum Schluss daran, dass Sie immer dann, wenn Sie ähnliche Anordnungsabschnitte in verschiedenen Tests sehen, versuchen sollten, diese in gemeinsam genutzte Hilfsmethoden zu integrieren, damit Ihre Tests in Zukunft trockener und wartbarer sind.


3

Ich mache das jetzt. AAAA einer anderen Art

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Beispiel für einen Update-Test:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

Der Grund dafür ist, dass die ACT das Lesen des ReadUpdated nicht enthält, weil es nicht Teil der Handlung ist. Die Handlung ändert sich nur und spart. Also wirklich, ARRANGE ReadUpdated für die Behauptung, ich rufe ASSEMBLE für die Behauptung an. Dies soll verhindern, dass der Abschnitt ARRANGE verwirrt wird

ASSERT sollte nur Zusicherungen enthalten. Dadurch bleibt ASSEMBLE zwischen ACT und ASSERT, wodurch der Assert eingerichtet wird.

Wenn Sie in der Anordnung fehlschlagen, sind Ihre Tests nicht korrekt, da Sie andere Tests haben sollten, um diese trivialen Fehler zu verhindern / zu finden . Denn für das von mir vorgestellte Szenario sollte es bereits andere Tests geben, die READ und CREATE testen. Wenn Sie eine "Guard Assertion" erstellen, brechen Sie möglicherweise DRY und erstellen Wartungsarbeiten.


1

Es ist eine alte Technik, eine "Sanity Check" -Ansicherung einzugeben, um den Status zu überprüfen, bevor Sie die zu testende Aktion ausführen. Normalerweise schreibe ich sie als Testgerüst, um mir selbst zu beweisen, dass der Test das tut, was ich erwarte, und entferne sie später, um Unordnungstests mit Testgerüsten zu vermeiden. Manchmal hilft das Belassen des Gerüsts, den Test als Erzählung zu verwenden.


1

Ich habe bereits über diese Technik gelesen - möglicherweise übrigens von Ihnen - aber ich benutze sie nicht; hauptsächlich, weil ich für meine Unit-Tests an das Triple-A-Formular gewöhnt bin.

Jetzt werde ich neugierig und habe einige Fragen: Wie schreiben Sie Ihren Test, führen Sie dazu, dass diese Behauptung nach einem Rot-Grün-Rot-Grün-Refaktor-Zyklus fehlschlägt, oder fügen Sie sie anschließend hinzu?

Scheitern Sie manchmal, vielleicht nachdem Sie den Code überarbeitet haben? Was sagt dir das? Vielleicht könnten Sie ein Beispiel nennen, bei dem es geholfen hat. Vielen Dank.


Normalerweise erzwinge ich nicht, dass die anfängliche Zusicherung fehlschlägt - schließlich sollte sie nicht so fehlschlagen, wie es eine TDD-Zusicherung tun sollte, bevor ihre Methode geschrieben wird. ich kann schreiben Sie es, wenn ich es schreiben, vor , nur im normalen Verlauf des Tests zu schreiben, nicht danach. Ehrlich gesagt kann ich mich nicht daran erinnern, dass es fehlgeschlagen ist - vielleicht deutet das darauf hin, dass es Zeitverschwendung ist. Ich werde versuchen, ein Beispiel zu finden, aber ich habe derzeit keines im Sinn. Danke für die Fragen; Sie sind hilfreich.
Carl Manaster

1

Ich habe dies bereits getan, als ich einen fehlgeschlagenen Test untersucht habe.

Nach erheblichen Kopfkratzern stellte ich fest, dass die Ursache dafür war, dass die während "Anordnen" aufgerufenen Methoden nicht richtig funktionierten. Der Testfehler war irreführend. Ich habe nach dem Arrangement einen Assert hinzugefügt. Dies führte dazu, dass der Test an einer Stelle fehlschlug, an der das eigentliche Problem hervorgehoben wurde.

Ich denke, hier riecht es auch nach Code, wenn der Arrangier-Teil des Tests zu lang und kompliziert ist.


Ein kleiner Punkt: Ich würde das zu komplizierte Arrangieren eher als Designgeruch als als Codegeruch betrachten - manchmal ist das Design so, dass Sie das Gerät nur mit einem komplizierten Arrangement testen können. Ich erwähne es, weil diese Situation eine tiefere Lösung erfordert als ein einfacher Codegeruch.
Carl Manaster

1

Im Allgemeinen mag ich "Arrange, Act, Assert" sehr und verwende es als meinen persönlichen Standard. Das einzige, woran ich mich nicht erinnern kann, ist, das, was ich arrangiert habe, zu ändern, wenn die Behauptungen fertig sind. In den meisten Fällen ist dies nicht sehr ärgerlich, da die meisten Dinge automatisch über die Speicherbereinigung usw. verschwinden. Wenn Sie jedoch Verbindungen zu externen Ressourcen hergestellt haben, möchten Sie diese Verbindungen wahrscheinlich schließen, wenn Sie fertig sind Mit Ihren Behauptungen oder Sie haben viele einen Server oder eine teure Ressource da draußen, die an Verbindungen oder wichtigen Ressourcen festhält, die sie an andere weitergeben sollten. Dies ist besonders wichtig, wenn Sie sind zu den Entwicklern gehören, die TearDown oder TestFixtureTearDown nicht verwendennach einem oder mehreren Tests aufräumen. Natürlich ist "Anordnen, Handeln, Bestätigen" nicht dafür verantwortlich, dass ich nicht schließe, was ich öffne. Ich erwähne dieses "Gotcha" nur, weil ich noch kein gutes "A-Wort" -Synonym für "Entsorgen" gefunden habe, das ich empfehlen kann! Irgendwelche Vorschläge?


1
@ carlmanaster, du bist mir eigentlich nah genug! Ich stecke das in mein nächstes TestFixture, um es für die Größe anzuprobieren. Es ist wie diese kleine Erinnerung daran, was deine Mutter dir hätte beibringen sollen: "Wenn du es öffnest, schließe es! Wenn du es vermasselst, räum es auf!" Vielleicht kann jemand anderes es verbessern, aber zumindest beginnt es mit einem "a!" Danke für Ihren Vorschlag!
John Tobler

1
@ carlmanaster, ich habe "Annul" ausprobiert. Es ist besser als "Teardown" und es funktioniert irgendwie, aber ich suche immer noch nach einem anderen "A" -Wort, das genauso perfekt in meinem Kopf steckt wie "Arrange, Act, Assert". Vielleicht "Vernichten?!"
John Tobler

1
Jetzt habe ich also "Anordnen, annehmen, handeln, behaupten, vernichten". Hmmm! Ich mache die Dinge zu kompliziert, oder? Vielleicht sollte ich mich einfach küssen und zu "Anordnen, handeln und behaupten" zurückkehren.
John Tobler

1
Vielleicht ein R zum Zurücksetzen verwenden? Ich weiß, es ist kein A, aber es klingt wie ein Pirat, der sagt: Aaargh! und Reime mit Assert zurücksetzen: o
Marcel Valdez Orozco

1

Schauen Sie sich den Wikipedia-Eintrag zu Design by Contract an . Die Arrange-Act-Assert-Dreifaltigkeit ist ein Versuch, einige der gleichen Konzepte zu kodieren, und es geht darum, die Programmkorrektheit zu beweisen. Aus dem Artikel:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

Es gibt einen Kompromiss zwischen dem Aufwand für die Einrichtung und dem Mehrwert. AAA ist eine nützliche Erinnerung an die erforderlichen Mindestschritte, sollte jedoch niemanden davon abhalten, zusätzliche Schritte zu erstellen.


0

Abhängig von Ihrer Testumgebung / -sprache. Wenn jedoch etwas im Arrangier-Teil fehlschlägt, wird eine Ausnahme ausgelöst und der Test zeigt sie nicht an, anstatt den Act-Teil zu starten. Also nein, ich verwende normalerweise keinen zweiten Assert-Teil.

Für den Fall, dass Ihr Arrangier-Teil recht komplex ist und nicht immer eine Ausnahme auslöst, können Sie es möglicherweise in eine Methode einwickeln und einen eigenen Test dafür schreiben, damit Sie sicher sein können, dass es nicht fehlschlägt (ohne eine Ausnahme auslösen).


0

Ich benutze dieses Muster nicht, weil ich denke, dass ich so etwas mache wie:

Arrange
Assert-Not
Act
Assert

Kann sinnlos sein, weil Sie angeblich wissen, dass Ihr Arrangier-Teil korrekt funktioniert. Dies bedeutet, dass alles, was sich im Arrangier-Teil befindet, ebenfalls getestet werden muss oder einfach genug ist, um keine Tests zu benötigen.

Verwenden Sie das Beispiel Ihrer Antwort:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}

Ich fürchte, Sie verstehen meine Frage nicht wirklich. Bei der ersten Behauptung geht es nicht darum, Arrange zu testen. es wird lediglich sichergestellt, dass das Gesetz dazu führt, dass der Staat am Ende behauptet wird.
Carl Manaster

Mein Punkt ist, dass alles, was Sie in den Assert-Not-Teil einfügen, bereits im Arrange-Teil enthalten ist, da der Code im Arrange-Teil gründlich getestet wurde und Sie bereits wissen, was er tut.
Marcel Valdez Orozco

Aber ich glaube, dass der Assert-Not-Teil einen Wert hat, weil Sie sagen: Wenn der Arrange-Teil "die Welt" in "diesem Zustand" verlässt, wird mein "Akt" "die Welt" in diesem "neuen Zustand" verlassen. ;; und wenn sich die Implementierung des Codes, von dem der Arrange-Teil abhängt, ändert, wird auch der Test unterbrochen. Aber auch dies könnte gegen DRY sein, da Sie (sollten) auch Tests für den Code durchführen, von dem Sie im Arrangier-Teil abhängig sind.
Marcel Valdez Orozco

Vielleicht wäre eine solche Klausel in Projekten, in denen mehrere Teams (oder ein großes Team) an demselben Projekt arbeiten, ziemlich nützlich, sonst halte ich sie für unnötig und überflüssig.
Marcel Valdez Orozco

Wahrscheinlich wäre eine solche Klausel besser bei Integrationstests, Systemtests oder Abnahmetests, bei denen der Arrangier-Teil normalerweise von mehr als einer Komponente abhängt und es mehr Faktoren gibt, die dazu führen können, dass sich der Anfangszustand der Welt unerwartet ändert. Aber ich sehe keinen Platz dafür in Unit-Tests.
Marcel Valdez Orozco

0

Wenn Sie wirklich alles im Beispiel testen möchten, versuchen Sie weitere Tests ... wie:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Weil Ihnen sonst so viele Fehlermöglichkeiten fehlen ... zB nach dem Einschließen umfasst der Bereich nur 7 usw. Es gibt auch Tests für die Länge des Bereichs (um sicherzustellen, dass er nicht auch einen zufälligen Wert umfasst) und eine weitere Reihe von Tests, die ausschließlich darauf abzielen, 5 im Bereich zu erfassen ... was würden wir erwarten - eine Ausnahme im Bereich oder der Bereich, der unverändert bleibt?

Der Punkt ist jedenfalls, wenn es irgendwelche Annahmen in der Handlung gibt, die Sie testen möchten, stellen Sie sie in ihren eigenen Test, ja?


0

Ich benutze:

1. Setup
2. Act
3. Assert 
4. Teardown

Weil ein sauberes Setup sehr wichtig ist.

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.