Comment affirmer l'égalité sur deux classes sans méthode d'égalité?

111

Disons que j'ai une classe sans méthode equals (), dont je n'ai pas la source. Je veux affirmer l'égalité sur deux instances de cette classe.

Je peux faire plusieurs assertions:

assertEquals(obj1.getFieldA(), obj2.getFieldA());
assertEquals(obj1.getFieldB(), obj2.getFieldB());
assertEquals(obj1.getFieldC(), obj2.getFieldC());
...

Je n'aime pas cette solution parce que je n'obtiens pas une image complète de l'égalité si une affirmation précoce échoue.

Je peux comparer manuellement et suivre le résultat:

String errorStr = "";
if(!obj1.getFieldA().equals(obj2.getFieldA())) {
    errorStr += "expected: " + obj1.getFieldA() + ", actual: " + obj2.getFieldA() + "\n";
}
if(!obj1.getFieldB().equals(obj2.getFieldB())) {
    errorStr += "expected: " + obj1.getFieldB() + ", actual: " + obj2.getFieldB() + "\n";
}
...
assertEquals("", errorStr);

Cela me donne une image complète de l'égalité, mais c'est maladroit (et je n'ai même pas pris en compte d'éventuels problèmes de nullité). Une troisième option consiste à utiliser Comparator, mais compareTo () ne me dira pas quels champs ont échoué à l'égalité.

Existe-t-il une meilleure pratique pour obtenir ce que je veux de l'objet, sans sous-classer ni surcharger les égaux (ugh)?

Ryan Nelson
la source
Recherchez-vous une bibliothèque qui effectue une comparaison approfondie pour vous? comme deep-equals suggéré à stackoverflow.com/questions/1449001/… ?
Vikdor
3
Pourquoi avez-vous besoin de savoir pourquoi les deux instances ne sont pas égales. Habituellement, une implémentation de equalméthode indique seulement si deux instances sont égales, et nous ne nous soucions pas de savoir pourquoi les intances ne sont pas égales.
Bhesh Gurung
3
Je veux savoir quelles propriétés sont inégales afin de pouvoir les corriger. :)
Ryan Nelson
Tous Objectont une equalsméthode, vous vouliez probablement dire aucune méthode égale remplacée.
Steve Kuo
La meilleure façon de penser est d'utiliser une classe wrapper ou une sous-classe, puis de l'utiliser après avoir remplacé la méthode equals.
Thihara

Réponses:

66

Mockito propose une réflexion-matcher:

Pour la dernière version de Mockito, utilisez:

Assert.assertTrue(new ReflectionEquals(expected, excludeFields).matches(actual));

Pour les anciennes versions, utilisez:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));
Felvhage
la source
17
Cette classe est dans le package org.mockito.internal.matchers.apachecommons. État de la documentation Mockito: org.mockito.internal-> "Classes internes, à ne pas utiliser par les clients." Vous exposerez votre projet à un risque en utilisant cela. Cela peut changer dans n'importe quelle version de Mockito. Lire ici: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac
6
Utilisez Mockito.refEq()plutôt.
Jeremy Kao
1
Mockito.refEq()échoue lorsque les objets n'ont pas d'id set = (
cavpollo
1
@PiotrAleksanderChmielowski, désolé, lorsque vous travaillez avec Spring + JPA + Entities, l'objet Entity peut avoir un id (représentant le champ id de la table de base de données), donc quand il est vide (un nouvel objet pas encore stocké dans la base de données), refEqéchoue à comparer car la méthode de hashcode ne peut pas comparer les objets.
cavpollo
3
Cela fonctionne bien, mais les attentes et les réels sont dans le mauvais ordre. Il devrait être l'inverse.
pkawiak
48

Il y a beaucoup de bonnes réponses ici, mais j'aimerais aussi ajouter ma version. Ceci est basé sur Assertj.

import static org.assertj.core.api.Assertions.assertThat;

public class TestClass {

    public void test() {
        // do the actual test
        assertThat(actualObject)
            .isEqualToComparingFieldByFieldRecursively(expectedObject);
    }
}

MISE À JOUR: Dans assertj v3.13.2, cette méthode est obsolète comme indiqué par Woodz dans le commentaire. La recommandation actuelle est

public class TestClass {

    public void test() {
        // do the actual test
        assertThat(actualObject)
            .usingRecursiveComparison()
            .isEqualTo(expectedObject);
    }

}
Thomas
la source
3
Dans assertj v3.13.2, cette méthode est obsolète et la recommandation est maintenant à utiliser usingRecursiveComparison()avec isEqualTo(), de sorte que la ligne soitassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz
45

J'implémente généralement ce cas d'utilisation en utilisant org.apache.commons.lang3.builder.EqualsBuilder

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));
Abhijeet Kushe
la source
1
Gradle: androidTestCompile 'org.apache.commons: commons-lang3: 3.5'
Roel
1
Vous devez l'ajouter à votre fichier gradle sous "dependencies" lorsque vous voulez utiliser "org.apache.commons.lang3.builder.EqualsBuilder"
Roel
3
Cela ne donne aucune indication sur les champs exacts qui ne correspondent pas réellement.
Vadzim
1
@Vadzim J'ai utilisé le code ci-dessous pour obtenir cette Assert.assertEquals (ReflectionToStringBuilder.toString (attendu), ReflectionToStringBuilder.toString (réel));
Abhijeet Kushe
2
Celui-ci nécessite que tous les nœuds du graphe implémentent «equal» et «hashcode», ce qui rend cette méthode pratiquement inutile. IsEqualToComparingFieldByFieldRecursively d'AssertJ est celui qui fonctionne parfaitement dans mon cas.
John Zhang
12

Je sais que c'est un peu vieux, mais j'espère que ça aide.

Je rencontre le même problème que vous, donc, après enquête, j'ai trouvé peu de questions similaires à celle-ci, et, après avoir trouvé la solution, je réponds à la même chose dans celles-ci, car je pensais que cela pourrait aider les autres.

La réponse la plus votée (et non celle choisie par l'auteur) de cette question similaire , est la solution la plus appropriée pour vous.

Fondamentalement, cela consiste à utiliser la bibliothèque appelée Unitils .

Voici l'utilisation:

User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);

Ce qui passera même si la classe Userne s'implémente pas equals(). Vous pouvez voir plus d'exemples et une affirmation vraiment cool appelée assertLenientEqualsdans leur tutoriel .

lopezvit
la source
1
UnitilsSemble malheureusement être mort, voir stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams
8

Vous pouvez utiliser Apache commons lang ReflectionToStringBuilder

Vous pouvez soit spécifier les attributs que vous souhaitez tester un par un, ou mieux, exclure ceux que vous ne voulez pas:

String s = new ReflectionToStringBuilder(o, ToStringStyle.SHORT_PREFIX_STYLE)
                .setExcludeFieldNames(new String[] { "foo", "bar" }).toString()

Vous comparez ensuite les deux chaînes comme d'habitude. Pour ce qui est de la lenteur de la réflexion, je suppose que ce n'est que pour les tests, donc cela ne devrait pas être si important.

Matthew Farwell
la source
1
L'avantage supplémentaire de cette approche est que vous obtenez une sortie visuelle affichant les valeurs attendues et réelles sous forme de chaînes excluant les champs dont vous ne vous souciez pas.
fquinner
7

Si vous utilisez hamcrest pour vos assertions (assertThat) et que vous ne voulez pas extraire de bibliothèques de test supplémentaires, vous pouvez utiliser SamePropertyValuesAs.samePropertyValuesAspour assertir des éléments qui n'ont pas de méthode equals remplacée.

L'avantage est que vous n'avez pas à insérer encore un autre framework de test et cela donnera une erreur utile lorsque l'assertion échoue ( expected: field=<value> but was field=<something else>) au lieu d' expected: true but was falseutiliser quelque chose comme EqualsBuilder.reflectionEquals().

L'inconvénient est qu'il s'agit d'une comparaison superficielle et qu'il n'y a pas d'option pour exclure des champs (comme c'est le cas dans EqualsBuilder), vous devrez donc contourner les objets imbriqués (par exemple, supprimez-les et comparez-les indépendamment).

Meilleur cas:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
assertThat(actual, is(samePropertyValuesAs(expected)));

Affaire laide:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
SomeClass expected = buildExpected(); 
SomeClass actual = sut.doSomething();

assertThat(actual.getSubObject(), is(samePropertyValuesAs(expected.getSubObject())));    
expected.setSubObject(null);
actual.setSubObject(null);

assertThat(actual, is(samePropertyValuesAs(expected)));

Alors, choisissez votre poison. Framework supplémentaire (par exemple Unitils), erreur inutile (par exemple EqualsBuilder) ou comparaison superficielle (hamcrest).

Chapiteau
la source
1
Pour le travail SamePropertyValuesAs, vous devez ajouter la dépendance du projet hamcrest.org/JavaHamcrest/ ...
bigspawn
J'aime cette solution, car elle n'ajoute pas de dépendance supplémentaire "complètement * différente!
GhostCat
2

Certaines des méthodes de comparaison de réflexion sont peu profondes

Une autre option consiste à convertir l'objet en json et à comparer les chaînes.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;    
public static String getJsonString(Object obj) {
 try {
    ObjectMapper objectMapper = new ObjectMapper();
    return bjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
     } catch (JsonProcessingException e) {
        LOGGER.error("Error parsing log entry", e);
        return null;
    }
}
...
assertEquals(getJsonString(MyexpectedObject), getJsonString(MyActualObject))
Avraham Shalev
la source
2

En utilisant Shazamcrest , vous pouvez faire:

assertThat(obj1, sameBeanAs(obj2));
Leonel Sanches da Silva
la source
Pourquoi utiliser shazamcrest au lieu de hamcrest? stackoverflow.com/a/27817702/6648326
MasterJoe
1
@ MasterJoe2 Dans mes tests, reflectEqualsrenvoie falselorsque les objets ont des références différentes.
Leonel Sanches da Silva le
1

Comparez champ par champ:

assertNotNull("Object 1 is null", obj1);
assertNotNull("Object 2 is null", obj2);
assertEquals("Field A differs", obj1.getFieldA(), obj2.getFieldA());
assertEquals("Field B differs", obj1.getFieldB(), obj2.getFieldB());
...
assertEquals("Objects are not equal.", obj1, obj2);
EthanB
la source
1
C'est quelque chose que je ne veux pas faire, car un échec d'assertion précoce masquera les échecs possibles ci-dessous.
Ryan Nelson
2
Désolé, j'ai manqué cette partie de votre message ... Pourquoi une "image d'égalité totale" est-elle importante dans un environnement de test unitaire? Soit les champs sont tous égaux (test réussi), soit ils ne sont pas tous égaux (le test échoue).
EthanB
Je ne veux pas avoir à réexécuter le test pour découvrir si d'autres champs ne sont pas égaux. Je veux connaître d'avance tous les champs qui sont inégaux afin de pouvoir les aborder en même temps.
Ryan Nelson
1
L'affirmation de plusieurs champs dans un test ne serait pas considérée comme un véritable test «unitaire». Avec le développement traditionnel piloté par les tests (TDD), vous écrivez un petit test, puis juste assez de code pour le réussir. Avoir une affirmation par champ est la bonne façon de le faire, mais ne mettez pas toutes les assertions dans un seul test. Créez un test différent pour chaque assertion de champ qui vous intéresse. Cela vous permettra de voir toutes les erreurs avec tous les champs avec une seule exécution de la suite. Si cela est difficile, cela signifie probablement que votre code n'est pas assez modulaire en premier lieu et qu'il peut probablement être remanié en une solution plus propre.
Jesse Webb
C'est certainement une suggestion valable, et c'est la manière habituelle de résoudre plusieurs assertions en un seul test. Le seul défi ici est, je voudrais une vue holistique de l'objet. Autrement dit, je voudrais tester tous les champs simultanément pour vérifier que l'objet est dans un état valide. Ce n'est pas difficile à faire lorsque vous avez une méthode equals () remplacée (ce que je n'ai bien sûr pas dans cet exemple).
Ryan Nelson
1

Les assertions AssertJ peuvent être utilisées pour comparer les valeurs sans #equalsméthode correctement remplacée, par exemple:

import static org.assertj.core.api.Assertions.assertThat; 

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);
Pavel
la source
1

Cette question étant ancienne, je proposerai une autre approche moderne utilisant JUnit 5.

Je n'aime pas cette solution parce que je n'obtiens pas une image complète de l'égalité si une affirmation précoce échoue.

Avec JUnit 5, il existe une méthode appelée Assertions.assertAll()qui vous permettra de regrouper toutes les assertions de votre test, qui exécutera chacune d'elles et affichera toutes les assertions ayant échoué à la fin. Cela signifie que toutes les assertions qui échouent en premier n'arrêteront pas l'exécution des dernières assertions.

assertAll("Test obj1 with obj2 equality",
    () -> assertEquals(obj1.getFieldA(), obj2.getFieldA()),
    () -> assertEquals(obj1.getFieldB(), obj2.getFieldB()),
    () -> assertEquals(obj1.getFieldC(), obj2.getFieldC()));
Patelb
la source
0

Vous pouvez utiliser la réflexion pour «automatiser» le test d'égalité complet. vous pouvez implémenter le code de «suivi» d'égalité que vous avez écrit pour un seul champ, puis utiliser la réflexion pour exécuter ce test sur tous les champs de l'objet.

jtahlborn
la source
0

Il s'agit d'une méthode de comparaison générique, qui compare deux objets d'une même classe pour ses valeurs de champs (gardez à l'esprit que ceux-ci sont accessibles par la méthode get)

public static <T> void compare(T a, T b) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    AssertionError error = null;
    Class A = a.getClass();
    Class B = a.getClass();
    for (Method mA : A.getDeclaredMethods()) {
        if (mA.getName().startsWith("get")) {
            Method mB = B.getMethod(mA.getName(),null );
            try {
                Assert.assertEquals("Not Matched = ",mA.invoke(a),mB.invoke(b));
            }catch (AssertionError e){
                if(error==null){
                    error = new AssertionError(e);
                }
                else {
                    error.addSuppressed(e);
                }
            }
        }
    }
    if(error!=null){
        throw error ;
    }
}
Shantonu
la source
0

Je suis tombé sur un cas très similaire.

Je voulais comparer sur un test qu'un objet avait les mêmes valeurs d'attribut comme un autre, mais des méthodes telles que is(), refEq(), etc ne marcherait pas pour des raisons comme mon objet ayant une valeur nulle dans son idattribut.

C'était donc la solution que j'ai trouvée (enfin, un collègue a trouvé):

import static org.apache.commons.lang.builder.CompareToBuilder.reflectionCompare;

assertThat(reflectionCompare(expectedObject, actualObject, new String[]{"fields","to","be","excluded"}), is(0));

Si la valeur obtenue à partir de reflectionCompareest 0, cela signifie qu'ils sont égaux. Si c'est -1 ou 1, ils diffèrent sur certains attributs.

cavpollo
la source
0

J'ai eu exactement la même énigme lors du test unitaire d'une application Android, et la solution la plus simple que j'ai trouvée était simplement d'utiliser Gson pour convertir mes objets de valeur réelle et attendue jsonet les comparer sous forme de chaînes.

String actual = new Gson().toJson( myObj.getValues() );
String expected = new Gson().toJson( new MyValues(true,1) );

assertEquals(expected, actual);

L'avantage de cela par rapport à la comparaison manuelle champ par champ est que vous comparez tous vos champs, donc même si vous ajoutez plus tard un nouveau champ à votre classe, il sera automatiquement testé, par rapport à si vous utilisiez un tas de assertEquals()sur tous les champs, qui devraient ensuite être mis à jour si vous ajoutez plus de champs à votre classe.

jUnit affiche également les chaînes pour vous afin que vous puissiez voir directement où elles diffèrent. Vous ne savez pas à quel point la commande sur le terrain Gsonest fiable , cela pourrait être un problème potentiel.

Magnus W
la source
1
L'ordre des champs n'est pas garanti par Gson. Vous voudrez peut-être JsonParse les chaînes et comparer les JsonElements résultant de l'analyse
ozma
0

J'ai essayé toutes les réponses et rien n'a vraiment fonctionné pour moi.

J'ai donc créé ma propre méthode qui compare des objets java simples sans aller plus loin dans les structures imbriquées ...

La méthode retourne null si tous les champs correspondent ou une chaîne contenant des détails d'incompatibilité.

Seules les propriétés qui ont une méthode getter sont comparées.

Comment utiliser

        assertNull(TestUtils.diff(obj1,obj2,ignore_field1, ignore_field2));

Exemple de sortie en cas d'incohérence

La sortie affiche les noms de propriétés et les valeurs respectives des objets comparés

alert_id(1:2), city(Moscow:London)

Code (Java 8 et supérieur):

 public static String diff(Object x1, Object x2, String ... ignored) throws Exception{
        final StringBuilder response = new StringBuilder();
        for (Method m:Arrays.stream(x1.getClass().getMethods()).filter(m->m.getName().startsWith("get")
        && m.getParameterCount()==0).collect(toList())){

            final String field = m.getName().substring(3).toLowerCase();
            if (Arrays.stream(ignored).map(x->x.toLowerCase()).noneMatch(ignoredField->ignoredField.equals(field))){
                Object v1 = m.invoke(x1);
                Object v2 = m.invoke(x2);
                if ( (v1!=null && !v1.equals(v2)) || (v2!=null && !v2.equals(v1))){
                    response.append(field).append("(").append(v1).append(":").append(v2).append(")").append(", ");
                }
            }
        }
        return response.length()==0?null:response.substring(0,response.length()-2);
    }
Stan Sokolov
la source
0

Comme alternative pour junit-only, vous pouvez définir les champs sur null avant l'assertion égale:

    actual.setCreatedDate(null); // excludes date assertion
    expected.setCreatedDate(null);

    assertEquals(expected, actual);
Andrew Taran
la source
-1

Pouvez-vous mettre le code de comparaison que vous avez publié dans une méthode utilitaire statique?

public static String findDifference(Type obj1, Type obj2) {
    String difference = "";
    if (obj1.getFieldA() == null && obj2.getFieldA() != null
            || !obj1.getFieldA().equals(obj2.getFieldA())) {
        difference += "Difference at field A:" + "obj1 - "
                + obj1.getFieldA() + ", obj2 - " + obj2.getFieldA();
    }
    if (obj1.getFieldB() == null && obj2.getFieldB() != null
            || !obj1.getFieldB().equals(obj2.getFieldB())) {
        difference += "Difference at field B:" + "obj1 - "
                + obj1.getFieldB() + ", obj2 - " + obj2.getFieldB();
        // (...)
    }
    return difference;
}

Ensuite, vous pouvez utiliser cette méthode dans JUnit comme ceci:

assertEquals ("Les objets ne sont pas égaux", "", findDifferences (obj1, obj));

ce qui n'est pas maladroit et vous donne des informations complètes sur les différences, si elles existent (pas exactement sous la forme normale d'assertEqual mais vous obtenez toutes les informations, donc ça devrait être bon).

Kamil
la source
-1

Cela n'aidera pas l'OP, mais cela pourrait aider tous les développeurs C # qui se retrouvent ici ...

Comme Enrique l'a publié , vous devriez remplacer la méthode d'égalité.

Existe-t-il une meilleure pratique pour obtenir ce que je veux de l'objet, sans sous-classer ni surcharger les égaux (ugh)?

Ma suggestion est de ne pas utiliser de sous-classe. Utilisez une classe partielle.

Définitions partielles de classe (MSDN)

Donc, votre classe ressemblerait à ...

public partial class TheClass
{
    public override bool Equals(Object obj)
    {
        // your implementation here
    }
}

Pour Java, je serais d'accord avec la suggestion d'utiliser la réflexion. N'oubliez pas que vous devez éviter d'utiliser la réflexion autant que possible. Il est lent, difficile à déboguer et encore plus difficile à maintenir dans le futur car les IDE pourraient casser votre code en renommant un champ ou quelque chose comme ça. Faites attention!

Jesse Webb
la source
-1

De vos commentaires à d'autres réponses, je ne comprends pas ce que vous voulez.

Juste pour le plaisir de la discussion, disons que la classe a remplacé la méthode equals.

Ainsi, votre UT ressemblera à quelque chose comme:

SomeType expected = // bla
SomeType actual = // bli

Assert.assertEquals(expected, actual). 

Et vous avez terminé. De plus, vous ne pouvez pas obtenir une "image d 'égalité complète" si l' assertion échoue.

D'après ce que je comprends, vous dites que même si le type remplaçait égal à égal, cela ne vous intéresserait pas, car vous voulez obtenir une «image d'égalité complète». Il est donc inutile non plus d'étendre et de remplacer les égaux.

Vous avez donc des options: soit comparer propriété par propriété, en utilisant la réflexion ou des vérifications codées en dur, je suggérerais ce dernier. Ou: comparez les représentations lisibles par l'homme de ces objets.

Par exemple, vous pouvez créer une classe d'assistance qui sérialise le type que vous souhaitez comparer à un document XML et ensuite comparer le XML résultant! dans ce cas, vous pouvez voir visuellement ce qui est exactement égal et ce qui ne l'est pas.

Cette approche vous donnera l'opportunité d'avoir une vue d'ensemble mais elle est aussi relativement lourde (et un peu sujette aux erreurs au début).

Vitalité
la source
Il est possible que mon terme «image de pleine égalité» prête à confusion. Implémenter equals () résoudrait en effet le problème. Je suis intéressé à connaître tous les champs inégaux (pertinents pour l'égalité) en même temps, sans avoir à réexécuter le test. La sérialisation de l'objet est une autre possibilité, mais je n'ai pas nécessairement besoin d'un égal profond. Je voudrais utiliser les implémentations equals () des propriétés si possible.
Ryan Nelson
Génial! Vous pouvez absolument utiliser les égaux des propriétés, comme vous l'avez indiqué dans votre question. Il semble que ce soit la solution la plus simple dans ce cas, mais comme vous l'avez noté, le code peut être très désagréable.
Vitaliy
-3

Vous pouvez remplacer la méthode equals de la classe comme:

@Override
public int hashCode() {
    int hash = 0;
    hash += (app != null ? app.hashCode() : 0);
    return hash;
}

@Override
public boolean equals(Object object) {
    HubRule other = (HubRule) object;

    if (this.app.equals(other.app)) {
        boolean operatorHubList = false;

        if (other.operator != null ? this.operator != null ? this.operator
                .equals(other.operator) : false : true) {
            operatorHubList = true;
        }

        if (operatorHubList) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

Eh bien, si vous voulez comparer deux objets d'une classe, vous devez implémenter d'une manière ou d'une autre les égaux et la méthode de code de hachage

Enrique San Martín
la source
3
mais OP dit qu'il ne veut pas passer outre ses égaux, il veut un meilleur moyen
Ankur