Best Practice: JUnit-Klassenfelder in setUp () oder bei der Deklaration initialisieren?

120

Soll ich Klassenfelder bei einer solchen Deklaration initialisieren?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Oder in setUp () so?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Ich neige dazu, das erste Formular zu verwenden, weil es prägnanter ist und es mir ermöglicht, endgültige Felder zu verwenden. Wenn ich nicht brauchen , um den Setup () -Methode für Set-up zu verwenden, soll ich es immer noch verwenden, und warum?

Erläuterung: JUnit instanziiert die Testklasse einmal pro Testmethode. Das bedeutet list, dass einmal pro Test erstellt wird, unabhängig davon, wo ich es deklariere. Dies bedeutet auch, dass zwischen den Tests keine zeitlichen Abhängigkeiten bestehen. Die Verwendung von setUp () scheint also keine Vorteile zu haben. Die JUnit-FAQ enthält jedoch viele Beispiele, die eine leere Sammlung in setUp () initialisieren. Ich denke, es muss einen Grund geben.

Craig P. Motlin
quelle
2
Beachten Sie, dass sich die Antwort in JUnit 4 (in der Deklaration initialisieren) und JUnit 3 (setUp verwenden) unterscheidet. Dies ist die Wurzel der Verwirrung.
Nils von Barth
Siehe auch stackoverflow.com/questions/6094081/…
Grigory Kislin

Antworten:

99

Wenn Sie sich speziell über die Beispiele in den JUnit-FAQ wundern, wie z. B. die grundlegende Testvorlage , ist es meiner Meinung nach die beste Vorgehensweise, die getestete Klasse in Ihrer setUp-Methode (oder in einer Testmethode) zu instanziieren. .

Wenn die JUnit-Beispiele eine ArrayList in der setUp-Methode erstellen, testen sie alle das Verhalten dieser ArrayList mit Fällen wie testIndexOutOfBoundException, testEmptyCollection und dergleichen. Die Perspektive besteht darin, dass jemand eine Klasse schreibt und sicherstellt, dass sie richtig funktioniert.

Sie sollten wahrscheinlich dasselbe tun, wenn Sie Ihre eigenen Klassen testen: Erstellen Sie Ihr Objekt in setUp oder in einer Testmethode, damit Sie eine angemessene Ausgabe erhalten, wenn Sie es später brechen.

Wenn Sie dagegen eine Java-Auflistungsklasse (oder eine andere Bibliotheksklasse) in Ihrem Testcode verwenden, liegt dies wahrscheinlich nicht daran, dass Sie sie testen möchten, sondern nur daran, dass sie Teil des Testgeräts ist. In diesem Fall können Sie davon ausgehen, dass es wie beabsichtigt funktioniert, sodass das Initialisieren in der Deklaration kein Problem darstellt.

Für das, was es wert ist, arbeite ich an einer ziemlich großen, mehrere Jahre alten, von TDD entwickelten Codebasis. Wir initialisieren gewöhnlich Dinge in ihren Deklarationen im Testcode, und in den anderthalb Jahren, in denen ich an diesem Projekt teilgenommen habe, hat es nie ein Problem verursacht. Es gibt also zumindest einige anekdotische Beweise dafür, dass dies eine vernünftige Sache ist.

Moss Collum
quelle
45

Ich fing an, mich selbst zu graben und fand einen möglichen Vorteil der Verwendung setUp(). Wenn während der Ausführung von Ausnahmen Ausnahmen ausgelöst setUp()werden, druckt JUnit eine sehr hilfreiche Stapelverfolgung. Wenn andererseits während der Objektkonstruktion eine Ausnahme ausgelöst wird, heißt es in der Fehlermeldung lediglich, dass JUnit den Testfall nicht instanziieren konnte, und Sie sehen nicht die Zeilennummer, in der der Fehler aufgetreten ist, wahrscheinlich weil JUnit Reflektion verwendet, um den Test zu instanziieren Klassen.

Nichts davon gilt für das Beispiel des Erstellens einer leeren Sammlung, da dies niemals ausgelöst wird, aber es ist ein Vorteil der setUp()Methode.

Craig P. Motlin
quelle
18

Neben der Antwort von Alex B.

Es ist sogar erforderlich, die setUp-Methode zu verwenden, um Ressourcen in einem bestimmten Zustand zu instanziieren. Dies im Konstruktor zu tun, ist nicht nur eine Frage des Timings, sondern aufgrund der Art und Weise, wie JUnit die Tests ausführt, wird jeder Teststatus nach dem Ausführen eines Tests gelöscht.

JUnit erstellt zunächst Instanzen der Testklasse für jede Testmethode und startet die Tests, nachdem jede Instanz erstellt wurde. Vor dem Ausführen der Testmethode wird die Setup-Methode ausgeführt, in der ein Zustand vorbereitet werden kann.

Wenn der Datenbankstatus im Konstruktor erstellt würde, würden alle Instanzen den Datenbankstatus direkt nacheinander instanziieren, bevor die einzelnen Tests ausgeführt werden. Ab dem zweiten Test würden die Tests mit einem verschmutzten Zustand ausgeführt.

JUnits Lebenszyklus:

  1. Erstellen Sie für jede Testmethode eine andere Testklasseninstanz
  2. Wiederholen Sie dies für jede Testklasseninstanz: Rufen Sie setup auf und rufen Sie die Testmethode auf

Bei einigen Protokollierungen in einem Test mit zwei Testmethoden erhalten Sie: (Nummer ist der Hashcode)

  • Neue Instanz erstellen: 5718203
  • Neue Instanz erstellen: 5947506
  • Einrichtung: 5718203
  • TestOne: 5718203
  • Einrichtung: 5947506
  • TestTwo: 5947506
Jürgen Hannaert
quelle
3
Richtig, aber nicht zum Thema. Die Datenbank ist im Wesentlichen ein globaler Status. Dies ist kein Problem, mit dem ich konfrontiert bin. Es geht mir lediglich um die Ausführungsgeschwindigkeit ordnungsgemäß unabhängiger Tests.
Craig P. Motlin
Diese Initialisierungsreihenfolge gilt nur für JUnit 3, wo es eine wichtige Warnung ist. In JUnit 4 werden Testinstanzen träge erstellt, sodass die Initialisierung in der Deklaration oder in einer Setup-Methode beide zur Testzeit erfolgt. Auch für einmaliges Setup kann man @BeforeClassin JUnit 4 verwenden.
Nils von Barth
11

In Einheit 4:

  • Initialisieren Sie für die zu testende Klasse eine @BeforeMethode, um Fehler zu erkennen.
  • Initialisieren Sie für andere Klassen in der Deklaration ...
    • ... der Kürze halber und zum Markieren von Feldern final, genau wie in der Frage angegeben,
    • ... es sei denn, es ist eine komplexe Initialisierung , die fehlschlagen könnte. In diesem Fall @Beforekönnen Fehler erkannt werden.
  • Verwenden Sie für den globalen Status (insbesondere langsame Initialisierung wie bei einer Datenbank) @BeforeClass, aber seien Sie vorsichtig von Abhängigkeiten zwischen den Tests.
  • Die Initialisierung eines in einem einzelnen Test verwendeten Objekts sollte natürlich in der Testmethode selbst erfolgen.

Durch die Initialisierung einer @BeforeMethode oder Testmethode erhalten Sie eine bessere Fehlerberichterstattung bei Fehlern. Dies ist besonders nützlich, um die zu testende Klasse zu instanziieren (die möglicherweise unterbrochen wird), aber auch, um externe Systeme aufzurufen, z. B. den Zugriff auf das Dateisystem ("Datei nicht gefunden") oder eine Verbindung zu einer Datenbank herzustellen ("Verbindung abgelehnt").

Es ist akzeptabel , einen einfachen Standard zu haben und immer zu verwenden@Before (klare Fehler, aber ausführlich) oder immer in der Deklaration zu initialisieren (prägnant, aber verwirrende Fehler), da komplexe Codierungsregeln schwer zu befolgen sind und dies keine große Sache ist.

Das Initialisieren in setUpist ein Relikt von JUnit 3, in dem alle Testinstanzen eifrig initialisiert wurden. Dies führt zu Problemen (Geschwindigkeit, Speicher, Erschöpfung der Ressourcen), wenn Sie eine teure Initialisierung durchführen. Die beste Vorgehensweise bestand daher darin, eine teure Initialisierung setUpdurchzuführen, die nur ausgeführt wurde, wenn der Test ausgeführt wurde. Dies gilt nicht mehr, so dass die Verwendung viel weniger notwendig ist setUp.

Dies fasst mehrere andere Antworten zusammen, die die Lede begraben, insbesondere von Craig P. Motlin (Frage selbst und Selbstantwort), Moss Collum (Klasse im Test) und dsaff.

Nils von Barth
quelle
7

In JUnit 3 werden Ihre Feldinitialisierer einmal pro Testmethode ausgeführt, bevor Tests ausgeführt werden . Solange Ihre Feldwerte im Speicher klein sind, wenig Einrichtungszeit benötigen und keinen Einfluss auf den globalen Status haben, ist die Verwendung von Feldinitialisierern technisch in Ordnung. Wenn diese jedoch nicht zutreffen, verbrauchen Sie möglicherweise viel Speicher oder Zeit beim Einrichten Ihrer Felder, bevor der erste Test ausgeführt wird, und möglicherweise sogar nicht genügend Speicher. Aus diesem Grund legen viele Entwickler Feldwerte immer in der setUp () -Methode fest, wo dies immer sicher ist, auch wenn dies nicht unbedingt erforderlich ist.

Beachten Sie, dass in JUnit 4 die Initialisierung von Testobjekten unmittelbar vor dem Ausführen des Tests erfolgt. Daher ist die Verwendung von Feldinitialisierern sicherer und wird empfohlen.

dsaff
quelle
Interessant. Das Verhalten, das Sie zuerst beschrieben haben, gilt also nur für JUnit 3?
Craig P. Motlin
6

In Ihrem Fall (Erstellen einer Liste) gibt es in der Praxis keinen Unterschied. Im Allgemeinen ist es jedoch besser, setUp () zu verwenden, da dies Junit dabei hilft, Ausnahmen korrekt zu melden. Wenn im Konstruktor / Initialisierer eines Tests eine Ausnahme auftritt, ist dies ein Testfehler . Wenn jedoch während des Setups eine Ausnahme auftritt, ist es naheliegend, diese als Problem beim Einrichten des Tests zu betrachten, und junit meldet sie entsprechend.

amit
quelle
1
gut gesagt. Gewöhnen Sie sich einfach daran, immer in setUp () zu instanziieren, und Sie haben eine Frage weniger zu befürchten - z. B. wo soll ich meine fooBar instanziieren, wo meine Sammlung. Es ist eine Art Codierungsstandard, den Sie nur einhalten müssen. Nutzen Sie nicht mit Listen, sondern mit anderen Instanziierungen.
Olaf Kock
@Olaf Danke für die Infos zum Codierungsstandard, darüber hatte ich nicht nachgedacht. Ich stimme jedoch eher Moss Collums Idee eines Codierungsstandards zu.
Craig P. Motlin
5

Ich bevorzuge zuerst die Lesbarkeit, bei der die Setup-Methode meistens nicht verwendet wird. Ich mache eine Ausnahme, wenn ein grundlegender Einrichtungsvorgang lange dauert und bei jedem Test wiederholt wird.
An diesem Punkt verschiebe ich diese Funktionalität mithilfe der @BeforeClassAnmerkung in eine Setup-Methode (später optimieren).

Beispiel für die Optimierung mit der @BeforeClassSetup-Methode: Ich verwende dbunit für einige Datenbankfunktionstests. Die Setup-Methode ist dafür verantwortlich, die Datenbank in einen bekannten Zustand zu versetzen (sehr langsam ... 30 Sekunden - 2 Minuten, abhängig von der Datenmenge). Ich lade diese Daten in die mit kommentierte Setup-Methode und führe @BeforeClassdann 10 bis 20 Tests mit demselben Datensatz aus, anstatt die Datenbank in jedem Test neu zu laden / zu initialisieren.

Die Verwendung von Junit 3.8 (Erweiterung von TestCase wie in Ihrem Beispiel gezeigt) erfordert das Schreiben von etwas mehr Code als nur das Hinzufügen einer Anmerkung, aber das "Einmal vor dem Einrichten der Klasse ausführen" ist weiterhin möglich.

Alex B.
quelle
1
+1, weil ich auch die Lesbarkeit bevorzuge. Ich bin jedoch nicht davon überzeugt, dass der zweite Weg überhaupt eine Optimierung ist.
Craig P. Motlin
@Motlin Ich habe das Beispiel dbunit hinzugefügt, um zu verdeutlichen, wie Sie mit dem Setup optimieren können.
Alex B
Die Datenbank ist im Wesentlichen ein globaler Status. Das Verschieben des Datenbank-Setups nach setUp () ist also keine Optimierung. Es ist erforderlich, dass die Tests ordnungsgemäß abgeschlossen werden.
Craig P. Motlin
@ Alex B: Wie Motlin sagte, ist dies keine Optimierung. Sie ändern nur, wo im Code die Initialisierung durchgeführt wird, aber nicht wie oft oder wie schnell.
Eddie
Ich wollte die Verwendung der Annotation "@BeforeClass" implizieren. Bearbeiten des Beispiels zur Verdeutlichung.
Alex B
2

Da jeder Test unabhängig mit einer neuen Instanz des Objekts ausgeführt wird, macht es keinen Sinn, dass das setUp()Testobjekt einen internen Status hat, außer dem, der zwischen einem einzelnen Test und geteilt wird tearDown(). Dies ist ein Grund (zusätzlich zu den Gründen, die andere angegeben haben), dass es gut ist, die setUp()Methode zu verwenden.

Hinweis: Es ist eine schlechte Idee für ein JUnit-Testobjekt, den statischen Status beizubehalten! Wenn Sie statische Variablen in Ihren Tests für andere Zwecke als für Tracking- oder Diagnosezwecke verwenden, machen Sie einen Teil des Zwecks von JUnit ungültig, dh, die Tests können (und können) in beliebiger Reihenfolge ausgeführt werden, wobei jeder Test mit a ausgeführt wird frischer, sauberer Zustand.

Die Verwendung hat den Vorteil, setUp()dass Sie nicht bei jeder Testmethode Initialisierungscode ausschneiden und einfügen müssen und dass Sie keinen Test-Setup-Code im Konstruktor haben. In Ihrem Fall gibt es kaum einen Unterschied. Das Erstellen einer leeren Liste kann sicher erfolgen, während Sie sie anzeigen, oder im Konstruktor, da es sich um eine triviale Initialisierung handelt. Wie Sie und andere bereits betont haben, Exceptionsollte jedoch alles getan werden, was möglicherweise einen auslösen kann, setUp()damit Sie den Diagnosestapel-Dump erhalten, wenn er fehlschlägt.

In Ihrem Fall, in dem Sie nur eine leere Liste erstellen, würde ich genauso vorgehen, wie Sie es vorschlagen: Weisen Sie die neue Liste am Deklarationspunkt zu. Vor allem, weil Sie auf diese Weise die Möglichkeit haben, es zu markieren, finalwenn dies für Ihre Testklasse sinnvoll ist.

Eddie
quelle
1
+1, weil Sie die erste Person sind, die die Initialisierung der Liste während der Objektkonstruktion unterstützt, um sie als endgültig zu markieren. Das Zeug über statische Variablen ist jedoch nicht zum Thema der Frage.
Craig P. Motlin
@Motlin: stimmt, das Zeug über statische Variablen ist ein wenig unangebracht. Ich bin mir nicht sicher, warum ich das hinzugefügt habe, aber es schien zu der Zeit angemessen, eine Erweiterung dessen, was ich im ersten Absatz gesagt habe.
Eddie
Der Vorteil von finalwird jedoch in der Frage erwähnt.
Nils von Barth
0
  • Die konstanten Werte (Verwendung in Fixtures oder Assertions) sollten in ihren Deklarationen initialisiert werden und final(wie nie ändern)

  • Das zu testende Objekt sollte in der Setup-Methode initialisiert werden, da wir möglicherweise Dinge einstellen. Natürlich können wir jetzt vielleicht nichts einstellen, aber wir könnten es später einstellen. Das Instanziieren in der init-Methode würde die Änderungen erleichtern.

  • Abhängigkeiten des zu testenden Objekts, wenn diese verspottet werden, sollten nicht einmal von Ihnen selbst instanziiert werden: Heute können die Schein-Frameworks sie durch Reflexion instanziieren.

Ein Test ohne Abhängigkeit zum Verspotten könnte folgendermaßen aussehen:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Ein Test mit zu isolierenden Abhängigkeiten könnte folgendermaßen aussehen:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
davidxxx
quelle