web-dev-qa-db-fra.com

Comment JUnit vérifier que deux listes <E> contiennent les mêmes éléments dans le même ordre?

Le contexte

J'écris un test simple JUnit pour la classe MyObject.

Un MyObject peut être créé à partir d'une méthode de fabrique statique qui prend des varargs de String.

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

À tout moment pendant l'existence de MyObject, les clients peuvent inspecter les paramètres pour lesquels il a été créé sous la forme d'un Liste <E>, via la méthode .getComponents().

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

En d'autres termes, un MyObject mémorise et expose la liste des paramètres qui l'ont créé. Plus de détails sur ce contrat:

  • L'ordre de getComponents sera le même que celui choisi pour la création d'objet
  • Les doublons suivants Chaîne sont autorisés et conservés dans l'ordre.
  • Le comportement sur null n'est pas défini (un autre code garantit que null ne parviendra pas à l'usine)
  • Il n'y a aucun moyen de modifier la liste des composants après l'instanciation d'un objet.

J'écris un test simple qui crée un MyObject à partir d'une liste de String et vérifie qu'il peut retourne la même liste via .getComponents(). Je le fais immédiatement mais cela est supposé se produire à distance dans un chemin de code réaliste.

Code

Voici ma tentative:


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));

Question

  • Est Google GuavaIterables.elementsEqual() le meilleur moyen, à condition que je dispose de la bibliothèque dans mon chemin de construction, comparer ces deux listes? c’est quelque chose qui m’inquiète; devrais-je utiliser cette méthode d'assistance qui passe au-dessus d'un Iterable <E> .. vérifier la taille, puis itérer en cours d'exécution .equals() .. ou toute autre méthode suggérée par une recherche Internet? quel est le moyen canonique de comparer des listes de tests unitaires?

Idées facultatives que j'aimerais beaucoup

  • Le test de la méthode est-il conçu de manière raisonnable? Je ne suis pas un expert en JUnit!
  • Est-ce que .toArray() est le meilleur moyen de convertir un Liste <E> en un varargs de E?
71
Robottinosino

Je préfère utiliser Hamcrest car cela donne un bien meilleur rendement en cas d'échec

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

Au lieu de signaler

expected true, got false

ça va rapporter

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

IsIterableContainingInOrder.contain

Hamcrest

Selon le Javadoc:

Crée un adapteur pour Iterables qui correspond lorsqu'un passage unique sur l'Iterable examiné produit une série d'éléments, chacun logiquement égal à l'élément correspondant dans les éléments spécifiés. Pour une correspondance positive, l'itéré examiné doit être de la même longueur que le nombre d'éléments spécifiés.

Donc, le listUnderTest doit avoir le même nombre d'éléments et chaque élément doit correspondre aux valeurs attendues dans l'ordre.

51
John B

Pourquoi ne pas simplement utiliser List#equals?

assertEquals(argumentComponents, imapPathComponents);

Contrat de List#equals :

deux listes sont définies égales si elles contiennent les mêmes éléments dans le même ordre.

70
assylias

La méthode equals () sur votre implémentation List doit faire la comparaison élément par élément, donc

assertEquals(argumentComponents, returnedComponents);

est beaucoup plus facile.

10
DavW

org.junit.Assert.assertEquals() et org.junit.Assert.assertArrayEquals() font le travail.

Pour éviter les questions suivantes: Si vous souhaitez ignorer l’ordre, placez tous les éléments à définir, puis comparez: Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))

Cependant, si vous voulez simplement ignorer les doublons mais conserver l'ordre, vous listez avec LinkedHashSet.

Encore un autre conseil. L'astuce Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two)) fonctionne bien jusqu'à ce que la comparaison échoue. Dans ce cas, il affiche un message d'erreur avec des représentations sous forme de chaîne de vos ensembles qui peuvent prêter à confusion parce que l'ordre de l'ensemble n'est presque pas prévisible (du moins pour les objets complexes). Le truc que j’ai trouvé est donc d’envelopper la collection avec un ensemble trié au lieu de HashSet. Vous pouvez utiliser TreeSet avec un comparateur personnalisé.

9
AlexR

Pour une excellente lisibilité du code, Fest Assertions a le support Nice pour asserting lists

Donc dans ce cas, quelque chose comme:

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

Ou faites la liste attendue à un tableau, mais je préfère l'approche ci-dessus parce que c'est plus clair.

Assertions.assertThat(returnedComponents).containsExactly(argumentComponents.toArray());
5
crunchdog

assertTrue ()/assertFalse (): à utiliser uniquement pour affirmer un résultat booléen renvoyé

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

Vous voulez utiliser Assert.assertTrue() ou Assert.assertFalse() comme la méthode à tester renvoie une valeur boolean.
Lorsque la méthode renvoie un élément spécifique, tel que List, qui devrait contenir certains éléments attendus, l'assertion avec assertTrue() de cette façon: Assert.assertTrue(myActualList.containsAll(myExpectedList) est une anti motif.
Cela rend l'assertion facile à écrire, mais comme le test échoue, il est également difficile de déboguer car le testeur ne vous dira que quelque chose comme:

attendu true mais le résultat est false

Assert.assertEquals(Object, Object) dans JUnit4 ou Assertions.assertIterableEquals(Iterable, Iterable) dans JUnit 5: à utiliser uniquement en tant que equals() et toString() sont écrasés pour les classes (et profondément) des objets comparés

Cela est important car le test d'égalité dans l'assertion s'appuie sur equals() et le message d'échec de test repose sur toString() des objets comparés.
Comme String annule equals() et toString(), il est parfaitement correct d'affirmer le List<String> Avec assertEquals(Object,Object). Et à propos de cette question: vous devez remplacer equals() dans une classe, car cela a du sens en termes d'égalité d'objet, pas seulement pour faciliter les assertions dans un test avec JUnit.
Pour faciliter les assertions, vous avez d'autres moyens (que vous pourrez voir dans les points suivants de la réponse).

Est-ce que Guava est un moyen de réaliser/construire des assertions de tests unitaires?

Google Guava Iterables.elementsEqual () constitue-t-il le meilleur moyen de comparer ces deux listes, à condition que la bibliothèque soit dans mon chemin de génération?

Non ce n'est pas. Guava n'est pas une bibliothèque pour écrire des assertions de tests unitaires.
Vous n'en avez pas besoin pour écrire la plupart (tout ce que je pense) des tests unitaires.

Quel est le moyen canonique de comparer des listes pour des tests unitaires?

En tant que bonne pratique, je privilégie les bibliothèques d'assertion/matcher.

Je ne peux pas encourager JUnit à effectuer des assertions spécifiques car cela fournit vraiment trop peu de fonctionnalités et des fonctionnalités limitées: il effectue uniquement une assertion avec un égal égal.
Parfois, vous souhaitez autoriser un ordre quelconque dans les éléments, parfois, vous souhaitez autoriser tout élément de la correspondance attendue avec l'élément réel, et ainsi de suite.

Il est donc judicieux d’utiliser une bibliothèque d’assertions/matcher de tests unitaires telle que Hamcrest ou AssertJ.
La réponse actuelle fournit une solution de Hamcrest. Voici une solution AssertJ .

org.assertj.core.api.ListAssert.containsExactly() est ce dont vous avez besoin: il vérifie que le groupe actuel contient exactement les valeurs données et rien d'autre, dans l'ordre indiqué:

Vérifie que le groupe actuel contient exactement les valeurs données et rien d'autre, dans l'ordre.

Votre test pourrait ressembler à:

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");
}

Un bon point pour AssertJ est que déclarer List comme prévu est inutile: cela rend l'assertion plus claire et le code plus lisible:

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

Et si le test échoue:

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

vous recevez un message très clair tel que:

Java.lang.AssertionError:

Attendant:

<["Un", "Deux", "Trois"]>

contenir exactement (et dans le même ordre):

<["Un", "Deux"]>

mais certains éléments n'étaient pas attendus:

<["Trois"]>

Les bibliothèques d'assertions/matcher sont indispensables car elles vont vraiment plus loin

Supposons que MyObject ne stocke pas Strings mais Foos des instances telles que:

public class MyFooObject {

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

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

C'est un besoin très commun. Avec AssertJ, l'assertion reste simple à écrire. Mieux vous pouvez affirmer que le contenu de la liste est égal même si la classe des éléments ne remplace pas equals()/hashCode() alors que les méthodes JUnit exigent que:

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"));
}
3
davidxxx
  • Ma réponse à savoir si Iterables.elementsEqual est le meilleur choix:

Iterables.elementsEqual est suffisant pour comparer 2 Lists.

Iterables.elementsEqual est utilisé dans des scénarios plus généraux, il accepte des types plus généraux: Iterable. Autrement dit, vous pouvez même comparer un List avec un Set. (par ordre d'itération, c'est important)

Bien sûr, ArrayList et LinkedList définissent assez bien, vous pouvez appeler directement comme égal. Tandis que lorsque vous utilisez une liste non bien définie, Iterables.elementsEqual est le meilleur choix. Une chose à noter: Iterables.elementsEqual n'accepte pas null

  • Pour convertir une liste en tableau: Iterables.toArray est plus facile.

  • Pour les tests unitaires, je recommande d’ajouter liste vide à votre scénario de test.

1