Wie kann JUnit testen, ob zwei Listen <E> dieselben Elemente in derselben Reihenfolge enthalten?

77

Kontext

Ich schreibe einen einfachen JUnit- Test für die MyObjectKlasse.

A MyObjectkann aus einer statischen Factory - Methode erstellt werden , die ein varargs Takes String .

MyObject.ofComponents("Uno", "Dos", "Tres");

Während des Bestehens von MyObjectkönnen Clients jederzeit die Parameter überprüfen, mit denen sie in Form einer Liste <E> über die .getComponents()Methode erstellt wurden.

myObject.ofComponents(); // -> List<String>: { "Uno", "Dos", "Tres" }

Mit anderen Worten, a MyObjectmerkt sich die Liste der Parameter, die sie ins Leben gerufen haben, und legt sie offen. Weitere Details zu diesem Vertrag:

  • Die Reihenfolge von getComponentswird dieselbe sein wie die für die Objekterstellung ausgewählte
  • Doppelte nachfolgende String- Komponenten sind zulässig und werden der Reihe nach beibehalten
  • Das Verhalten bei nullist undefiniert (anderer Code garantiert, dass kein nullWerk erreicht wird)
  • Es gibt keine Möglichkeit, die Liste der Komponenten nach der Objektinstanziierung zu ändern

Ich schreibe einen einfachen Test, der MyObjectaus einer Liste von Zeichenfolgen einen erstellt und prüft, ob er dieselbe Liste über zurückgeben kann .getComponents(). Ich mache das sofort, aber das soll in einiger Entfernung in einem realistischen Codepfad passieren .

Code

Hier mein Versuch:


List<String> argumentComponents = Lists.newArrayList("One", "Two", "Three");
List<String> returnedComponents =
    MyObject.ofComponents(
        argumentComponents.toArray(new String[argumentComponents.size()]))
        .getComponents();
assertTrue(Iterables.elementsEqual(argumentComponents, returnedComponents));

Frage

  • Ist Google Guava Iterables.elementsEqual() der beste Weg, um diese beiden Listen zu vergleichen, vorausgesetzt, ich habe die Bibliothek in meinem Erstellungspfad? das ist etwas, worüber ich mich gequält habe; sollte ich diese Hilfsmethode verwenden, die über eine Iterable <E> .. Größe überprüft und dann die Ausführung wiederholt.equals() .. oder eine andere Methode, die eine Internetsuche vorschlägt? Wie kann man Listen für Unit-Tests kanonisch vergleichen?

Optionale Einblicke würde ich gerne bekommen

  • Ist der Methodentest angemessen ausgelegt? Ich bin kein Experte in JUnit !
  • Ist .toArray()der beste Weg, eine Liste <E> in eine Variable von E zu konvertieren ?
Robottinosino
quelle
13
+1 für gut strukturierte Frage.
Maba
1
Hamcrest hat IsIterableContainingInOrderdas genau zum Testen im Gegensatz zu Iterables. Wenn Sie Hamcrest verwenden, erhalten Sie im Fehlerfall gute Nachrichten.
John B
fest-assert bietet eine elegante fließende api ...
gontard
@ JohnB: Hamcrest ist großartig und ideal zum Testen, was ich gefragt habe (danke!), Aber in diesem speziellen Fall IsIterableContainingInOrderist es nicht streng genug, oder? M={A,B,C}enthält N={A,B}in der Reihenfolge aber M!=N.
Robottinosino
@Robottinosino Laut dem JavadocCreates a matcher for Iterables that matches when a single pass over the examined Iterable yields a series of items, each logically equal to the corresponding item in the specified items. For a positive match, the examined iterable must be of the same length as the number of specified items
John B

Antworten:

57

Ich bevorzuge die Verwendung von Hamcrest, da es im Fehlerfall eine viel bessere Ausgabe liefert

Assert.assertThat(listUnderTest, 
       IsIterableContainingInOrder.contains(expectedList.toArray()));

Anstatt zu berichten

expected true, got false

es wird berichten

expected List containing "1, 2, 3, ..." got list containing "4, 6, 2, ..."

IsIterableContainingInOrder.contain

Hamcrest

Nach Angaben des Javadoc:

Erstellt einen Matcher für Iterables, der übereinstimmt, wenn ein einzelner Durchlauf über das untersuchte Iterable eine Reihe von Elementen ergibt, die jeweils logisch dem entsprechenden Element in den angegebenen Elementen entsprechen. Für eine positive Übereinstimmung muss die untersuchte Iterable dieselbe Länge wie die Anzahl der angegebenen Elemente haben

Das listUnderTestElement muss also die gleiche Anzahl von Elementen haben und jedes Element muss in der angegebenen Reihenfolge mit den erwarteten Werten übereinstimmen.

John B.
quelle
1
Ist IsterableContainingInOrder nicht streng genug für die Gleichheitsprüfung?
Robottinosino
was meinst du / was fragst du
John B
Die Ausgabe ist für Hamcrest dieselbe wie für normale jUnit-Zusicherungen. assertEquals (erwartet, tatsächlich); wird ausgeben: java.lang.AssertionError: erwartet: <[1, 2]> aber war: <[3, 4]> Ich glaube nicht, dass Hamcrest irgendwelche Vorteile in
Bezug auf
@henrik - Eigentlich wird von Hamcrest ausgegeben Expected: iterable containing [<1>, <3>, <5>, <7>] but: item 3: was <6>. In einer langen Liste von Elementen, deren toStringAusgabe sehr lang sein kann, kann es sehr hilfreich sein, zu wissen, welches Element falsch ist.
John B
1
IsIterableContainingInOrder.contain()ist irreführender Name. Es spiegelt nicht die Tatsache wider, dass zusätzlich actualüberprüft wird, ob die gleiche Anzahl von Elementen vorhanden ist (Überprüfung der Größengleichheit). hasSameElementsAs()wäre passender Name
Lu55
75

Warum nicht einfach benutzen List#equals?

assertEquals(argumentComponents, imapPathComponents);

Vertrag vonList#equals :

Zwei Listen werden als gleich definiert, wenn sie dieselben Elemente in derselben Reihenfolge enthalten.

Assylien
quelle
Welcher Matcher von Hamcrest? Ich könnte präzise Meldungen über Assertionsfehler verwenden, die kostenlos zur Verfügung gestellt werden ...
Robottinosino
1
Wie für die Verwendung ohne externe Bibliotheken
Spektakulatius
IMHO sieht dieser Code viel klarer aus als der von Hamcrest IsIterableContainingInOrder.contain()(dessen Name sein Verhalten nicht vollständig widerspiegelt). Wenn Sie also andere Sammlungen als List <> haben, ist es besser, diese in die Liste zu konvertieren und diesen Code zu verwenden, als ein Array, das von Hamcrest benötigt wird.
Lu55
10

Die equals () -Methode in Ihrer List-Implementierung sollte also einen elementweisen Vergleich durchführen

assertEquals(argumentComponents, returnedComponents);

ist viel einfacher.

DavW
quelle
10

org.junit.Assert.assertEquals()und org.junit.Assert.assertArrayEquals()mach den Job.

So vermeiden Sie die nächsten Fragen: Wenn Sie die Reihenfolge ignorieren möchten, setzen Sie alle Elemente und vergleichen Sie sie: Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))

Wenn Sie jedoch nur Duplikate ignorieren möchten, aber den von Ihnen aufgelisteten Auftragsumbruch beibehalten möchten LinkedHashSet.

Noch ein Tipp. Der Trick Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))funktioniert einwandfrei, bis der Vergleich fehlschlägt. In diesem Fall wird eine Fehlermeldung mit Zeichenfolgendarstellungen Ihrer Mengen angezeigt, die verwirrend sein kann, da die Reihenfolge in der Menge (zumindest für komplexe Objekte) fast nicht vorhersehbar ist. Der Trick, den ich gefunden habe, besteht darin, die Sammlung mit einem sortierten Satz anstatt zu umschließen HashSet. Sie können TreeSetmit benutzerdefinierten Komparator verwenden.

AlexR
quelle
7
Bei Verwendung eines HashSet werden die Reihenfolge und die Duplikate nicht beibehalten .
Pontard
5

Für eine hervorragende Lesbarkeit des Codes bietet Fest Assertions eine gute Unterstützung für das Durchsetzen von Listen

Also in diesem Fall so etwas wie:

Assertions.assertThat(returnedComponents).containsExactly("One", "Two", "Three");

Oder machen Sie die erwartete Liste zu einem Array, aber ich bevorzuge den obigen Ansatz, weil es klarer ist.

Assertions.assertThat(returnedComponents).containsExactly(argumentComponents.toArray());
Crunchdog
quelle
3

assertTrue () / assertFalse (): Nur zum Bestätigen des zurückgegebenen booleschen Ergebnisses

assertTrue (Iterables.elementsEqual (argumentComponents, returnComponents));

Sie möchten Assert.assertTrue()oder Assert.assertFalse()als zu testende Methode einen booleanWert zurückgeben.
Da die Methode eine bestimmte Sache zurückgibt, z. B. eine List, die einige erwartete Elemente enthalten sollte, wird Folgendes behauptet assertTrue(): Assert.assertTrue(myActualList.containsAll(myExpectedList) ist ein Anti-Muster.
Es macht das Schreiben der Behauptung einfach, aber wenn der Test fehlschlägt, macht es auch das Debuggen schwierig, da der Testläufer Ihnen nur Folgendes sagt:

erwartet trueaber aktuell istfalse

Assert.assertEquals(Object, Object)in JUnit4 oder Assertions.assertIterableEquals(Iterable, Iterable)in JUnit 5: Nur als beides verwenden equals()und toString()für die Klassen (und tief) der verglichenen Objekte überschrieben werden

Dies ist wichtig, da sich der Gleichheitstest in der Zusicherung equals()auf toString()die verglichenen Objekte und die Testfehlermeldung auf diese stützt .
Da Stringüberschreibt beide equals()und toString(), ist es vollkommen gültig, das List<String>mit zu behaupten assertEquals(Object,Object). Und zu diesem Thema: Sie müssen equals()in einer Klasse überschreiben, weil dies im Hinblick auf die Objektgleichheit sinnvoll ist, nicht nur, um Aussagen in einem Test mit JUnit zu vereinfachen.
Um die Behauptungen zu vereinfachen, haben Sie andere Möglichkeiten (die Sie in den nächsten Punkten der Antwort sehen können).

Ist Guave eine Möglichkeit, Unit-Test-Assertions durchzuführen / zu erstellen?

Ist Google Guava Iterables.elementsEqual () der beste Weg, um diese beiden Listen zu vergleichen, vorausgesetzt, ich habe die Bibliothek in meinem Erstellungspfad?

Nein ist es nicht. Guava ist keine Bibliothek zum Schreiben von Unit-Test-Assertions.
Sie brauchen es nicht, um die meisten (alles, was ich denke) Unit-Tests zu schreiben.

Was ist der kanonische Weg, um Listen für Unit-Tests zu vergleichen?

Als gute Praxis bevorzuge ich Assertion / Matcher-Bibliotheken.

Ich kann JUnit nicht dazu ermutigen, bestimmte Behauptungen aufzustellen, da dies wirklich zu wenige und eingeschränkte Funktionen bietet: Es führt nur eine Behauptung mit einer tiefen Gleichheit aus.
Manchmal möchten Sie eine beliebige Reihenfolge in den Elementen zulassen, manchmal möchten Sie zulassen, dass alle Elemente der erwarteten mit der tatsächlichen übereinstimmen, und so weiter für ...

Die Verwendung einer Unit-Test-Assertion / Matcher-Bibliothek wie Hamcrest oder AssertJ ist daher der richtige Weg.
Die eigentliche Antwort bietet eine Hamcrest-Lösung. Hier ist eine AssertJ- Lösung.

org.assertj.core.api.ListAssert.containsExactly() ist das, was Sie brauchen: Es überprüft, ob die tatsächliche Gruppe genau die angegebenen Werte und nichts anderes enthält, in der angegebenen Reihenfolge:

Überprüft, ob die tatsächliche Gruppe genau die angegebenen Werte enthält und nichts anderes in der angegebenen Reihenfolge.

Ihr Test könnte folgendermaßen aussehen:

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

@Test
void ofComponent_AssertJ() throws Exception {
   MyObject myObject = MyObject.ofComponents("One", "Two", "Three");
   Assertions.assertThat(myObject.getComponents())
             .containsExactly("One", "Two", "Three");
}

Ein guter Punkt von AssertJ ist, dass es Listunnötig ist, a wie erwartet zu deklarieren : Dadurch wird die Assertion gerader und der Code besser lesbar:

Assertions.assertThat(myObject.getComponents())
         .containsExactly("One", "Two", "Three");

Und wenn der Test fehlschlägt:

// Fail : Three was not expected 
Assertions.assertThat(myObject.getComponents())
          .containsExactly("One", "Two");

Sie erhalten eine sehr klare Nachricht wie:

java.lang.AssertionError:

Erwarten:

<["Eins", "Zwei", "Drei"]>

genau (und in derselben Reihenfolge) enthalten:

<["Eins", "Zwei"]>

Einige Elemente wurden jedoch nicht erwartet:

<["Drei"]>

Assertion / Matcher-Bibliotheken sind ein Muss, da diese wirklich weiter gehen

Angenommen, MyObjectes werden keine s-Instanzen gespeichert, Stringsondern Foos-Instanzen wie:

public class MyFooObject {

    private List<Foo> values;
    @SafeVarargs
    public static MyFooObject ofComponents(Foo... values) {
        // ...
    }

    public List<Foo> getComponents(){
        return new ArrayList<>(values);
    }
}

Das ist ein sehr häufiges Bedürfnis. Mit AssertJ ist die Behauptung immer noch einfach zu schreiben. Besser können Sie behaupten, dass der Listeninhalt gleich ist, auch wenn die Klasse der Elemente nicht überschrieben wird, equals()/hashCode()während JUnit-Methoden Folgendes erfordern:

import org.assertj.core.api.Assertions;
import static org.assertj.core.groups.Tuple.tuple;
import org.junit.jupiter.api.Test;

@Test
void ofComponent() throws Exception {
    MyFooObject myObject = MyFooObject.ofComponents(new Foo(1, "One"), new Foo(2, "Two"), new Foo(3, "Three"));

    Assertions.assertThat(myObject.getComponents())
              .extracting(Foo::getId, Foo::getName)
              .containsExactly(tuple(1, "One"),
                               tuple(2, "Two"),
                               tuple(3, "Three"));
}
davidxxx
quelle
1
  • Meine Antwort, ob Iterables.elementsEqualdie beste Wahl ist:

Iterables.elementsEqualreicht aus, um 2 Lists zu vergleichen .

Iterables.elementsEqualwird in allgemeineren Szenarien verwendet. Es werden allgemeinere Typen akzeptiert : Iterable. Das heißt, Sie könnten sogar a Listmit a vergleichen Set. (In iterativer Reihenfolge ist es wichtig)

Sicher ArrayListund LinkedListgleich definieren ziemlich gut, man könnte gleich gleich nennen. Wenn Sie eine nicht genau definierte Liste verwenden, Iterables.elementsEqualist dies die beste Wahl. Eines sollte beachtet werden: Iterables.elementsEqualakzeptiert nichtnull

  • So konvertieren Sie Liste in Array: Iterables.toArrayist einfacher.

  • Für Unit-Tests empfehle ich , Ihrem Testfall eine leere Liste hinzuzufügen .

Gy 声 远 Shengyuan Lu
quelle