Come si afferma l'uguaglianza su due classi senza un metodo di uguaglianza?


111

Supponiamo di avere una classe senza il metodo equals (), di cui non ho il sorgente. Voglio affermare l'uguaglianza su due istanze di quella classe.

Posso fare più affermazioni:

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

Non mi piace questa soluzione perché non ottengo il quadro completo dell'uguaglianza se un'asserzione iniziale fallisce.

Posso confrontare manualmente da solo e monitorare il risultato:

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

Questo mi dà il quadro completo dell'uguaglianza, ma è goffo (e non ho nemmeno tenuto conto di possibili problemi nulli). Una terza opzione è usare Comparator, ma compareTo () non mi dirà quali campi hanno fallito l'uguaglianza.

Esiste una pratica migliore per ottenere ciò che voglio dall'oggetto, senza sottoclassare e sovrascrivere gli uguali (ugh)?


Stai cercando una libreria che faccia un confronto profondo per te? come deep-equals suggerito su stackoverflow.com/questions/1449001/… ?
Vikdor

3
Perché hai bisogno di sapere perché le due istanze non erano uguali. Di solito, un'implementazione del equalmetodo dice solo se due istanze sono uguali e non ci interessa perché le intance non sono uguali.
Bhesh Gurung

3
Voglio sapere quali proprietà sono disuguali in modo da poterle correggere. :)
Ryan Nelson

Tutti Objecthanno un equalsmetodo, probabilmente non intendevi nessun metodo uguale a sovrascrittura.
Steve Kuo

Il modo migliore a cui posso pensare è usare una classe wrapper o una sottoclasse e poi usarla dopo aver sovrascritto il metodo uguale ..
Thihara

Risposte:


66

Mockito offre un abbinamento di riflessioni:

Per l'ultima versione di Mockito utilizzare:

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

Per le versioni precedenti utilizzare:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));

17
Questa classe è nel pacchetto org.mockito.internal.matchers.apachecommons. I documenti di Mockito affermano: org.mockito.internal-> "Classi interne, non utilizzabili dai client." Metterete a rischio il vostro progetto usando questo. Questo può cambiare in qualsiasi versione di Mockito. Leggi qui: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac

6

1
Mockito.refEq()fallisce quando gli oggetti non hanno un id impostato = (
cavpollo

1
@PiotrAleksanderChmielowski, mi dispiace, quando si lavora con Spring + JPA + Entities, l'oggetto Entity potrebbe avere un id (che rappresenta il campo id della tabella del database), quindi quando è vuoto (un nuovo oggetto non ancora memorizzato nel DB), refEqfallisce da confrontare poiché il metodo hashcode non è in grado di confrontare gli oggetti.
cavpollo

3
Funziona bene, ma le previsioni e le effettive sono nell'ordine sbagliato. Dovrebbe essere il contrario.
pkawiak

48

Ci sono molte risposte corrette qui, ma vorrei aggiungere anche la mia versione. Questo è basato su Assertj.

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

public class TestClass {

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

AGGIORNAMENTO: in assertj v3.13.2 questo metodo è deprecato come sottolineato da Woodz nel commento. La raccomandazione attuale è

public class TestClass {

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

}

3
In assertj v3.13.2 questo metodo è deprecato e la raccomandazione è ora di utilizzo usingRecursiveComparison()con isEqualTo(), in modo tale che la linea èassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz

45

In genere implemento questo caso d'uso utilizzando org.apache.commons.lang3.builder.EqualsBuilder

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));

1
Gradle: androidTestCompile 'org.apache.commons: commons-lang3: 3.5'
Roel

1
È necessario aggiungerlo al file gradle in "dipendenze" quando si desidera utilizzare "org.apache.commons.lang3.builder.EqualsBuilder"
Roel

3
Questo non dà alcun suggerimento su quali campi esatti non corrispondessero effettivamente.
Vadzim

1
@Vadzim ho usato il codice seguente per ottenere quello Assert.assertEquals (ReflectionToStringBuilder.toString (previsto), ReflectionToStringBuilder.toString (effettivo));
Abhijeet Kushe

2
Questo richiede che tutti i nodi nel grafo implementino "uguale" e "hashcode", che fondamentalmente rendono questo metodo quasi inutile. IsEqualToComparingFieldByFieldRecursively di AssertJ è quello che funziona perfettamente nel mio caso.
John Zhang

12

So che è un po 'vecchio, ma spero che aiuti.

Mi imbatto nello stesso problema che hai tu, quindi, dopo un'indagine, ho trovato poche domande simili a questa e, dopo aver trovato la soluzione, sto rispondendo allo stesso modo in quelle, poiché pensavo che potesse aiutare gli altri.

La risposta più votata (non quella scelta dall'autore) a questa domanda simile , è la soluzione più adatta a te.

Fondamentalmente, consiste nell'usare la libreria chiamata Unitils .

Questo è l'uso:

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

Che passerà anche se la classe Usernon implementa equals(). Puoi vedere più esempi e un'affermazione davvero interessante chiamata assertLenientEqualsnel loro tutorial .


1
Purtroppo Unitilssembra essere morto, vedi stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams

8

Puoi usare Apache commons lang ReflectionToStringBuilder

Puoi specificare gli attributi che desideri testare uno per uno o, meglio, escludere quelli che non desideri:

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

Quindi confrontare le due stringhe normalmente. Per quanto riguarda la lentezza della riflessione, presumo che sia solo per il test, quindi non dovrebbe essere così importante.


1
Il vantaggio aggiuntivo di questo approccio è che si ottiene un output visivo che mostra i valori attesi ed effettivi come stringhe che escludono i campi che non interessano.
fquinner

7

Se stai usando hamcrest per le tue asserzioni (assertThat) e non vuoi inserire librerie di test aggiuntive, puoi usare SamePropertyValuesAs.samePropertyValuesAsper asserire elementi che non hanno un metodo uguale a sovrascritto.

Il vantaggio è che non devi inserire ancora un altro framework di test e darà un errore utile quando l'assert fallisce ( expected: field=<value> but was field=<something else>) invece che expected: true but was falsese usi qualcosa di simile EqualsBuilder.reflectionEquals().

Lo svantaggio è che è un confronto superficiale e non c'è alcuna opzione per escludere i campi (come in EqualsBuilder), quindi dovrai aggirare gli oggetti nidificati (ad esempio rimuoverli e confrontarli in modo indipendente).

Caso migliore:

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

Brutto caso:

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

Quindi, scegli il tuo veleno. Framework aggiuntivo (ad esempio Unitils), errore inutile (ad esempio EqualsBuilder) o confronto superficiale (hamcrest).


1
Per lavoro SamePropertyValuesAs è necessario aggiungere in dipendenza del progetto hamcrest.org/JavaHamcrest/…
bigspawn

Mi piace questa soluzione, poiché non aggiunge una "dipendenza aggiuntiva completamente * diversa!"
GhostCat


2

Alcuni dei metodi di confronto delle riflessioni sono superficiali

Un'altra opzione è convertire l'oggetto in un json e confrontare le stringhe.

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


1

Confronta campo per campo:

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

1
Questo è qualcosa che non voglio fare, perché un fallimento iniziale di asserzione nasconderà possibili errori di seguito.
Ryan Nelson,

2
Scusa, mi sono persa quella parte del tuo post ... Perché un "quadro completo di uguaglianza" è importante in un ambiente di test unitario? O i campi sono tutti uguali (test superato) o non sono tutti uguali (test fallito).
EthanB

Non voglio dover rieseguire il test per scoprire se altri campi non sono uguali. Voglio conoscere in anticipo tutti i campi che non sono uguali in modo da poterli affrontare contemporaneamente.
Ryan Nelson,

1
Affermare molti campi in un test non sarebbe considerato un vero test "unitario". Con il tradizionale sviluppo basato su test (TDD), scrivi un piccolo test e quindi solo il codice sufficiente per farlo passare. Avere un'asserzione per campo è il modo corretto per farlo, ma non mettere tutte le affermazioni in un unico test. Crea un test diverso per ogni asserzione di campo che ti interessa. Questo ti permetterà di vedere tutti gli errori con tutti i campi con una singola esecuzione della suite. Se questo è difficile, probabilmente significa che il tuo codice non è abbastanza modulare in primo luogo e può probabilmente essere riformattato in una soluzione più pulita.
Jesse Webb

Questo è sicuramente un suggerimento valido ed è il solito modo in cui risolveresti più affermazioni in un singolo test. L'unica sfida qui è che vorrei una visione olistica dell'oggetto. Cioè, vorrei testare tutti i campi contemporaneamente per verificare che l'oggetto sia in uno stato valido. Questo non è difficile da fare quando hai un metodo equals () sovrascritto (che ovviamente non ho in questo esempio).
Ryan Nelson

1

Le asserzioni AssertJ possono essere utilizzate per confrontare i valori senza che il #equalsmetodo venga adeguatamente sovrascritto, ad esempio:

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

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);

1

Poiché questa domanda è vecchia, suggerirò un altro approccio moderno utilizzando JUnit 5.

Non mi piace questa soluzione perché non ottengo il quadro completo dell'uguaglianza se un'asserzione iniziale fallisce.

Con JUnit 5, esiste un metodo chiamato Assertions.assertAll()che ti consentirà di raggruppare tutte le asserzioni nel tuo test e le eseguirà ciascuna e produrrà tutte le asserzioni fallite alla fine. Ciò significa che qualsiasi affermazione che fallisce per prima non interromperà l'esecuzione di queste ultime.

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

0

È possibile utilizzare la riflessione per "automatizzare" il test di uguaglianza completo. puoi implementare il codice di "monitoraggio" di uguaglianza che hai scritto per un singolo campo, quindi utilizzare la reflection per eseguire il test su tutti i campi dell'oggetto.


0

Questo è un metodo di confronto generico, che confronta due oggetti di una stessa classe per i valori dei suoi campi (tieni presente che questi sono accessibili tramite il metodo 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 ;
    }
}

0

Mi sono imbattuto in un caso molto simile.

Ho voluto confrontare su una prova che un oggetto aveva gli stessi valori degli attributi come un altro, ma metodi come is(), refEq(), ecc non avrebbe funzionato per motivi come il mio oggetto che ha un valore nullo nel suo idattributo.

Quindi questa è stata la soluzione che ho trovato (beh, un collega ha trovato):

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

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

Se il valore ottenuto da reflectionCompareè 0, significa che sono uguali. Se è -1 o 1, differiscono su alcuni attributi.



0

Ho avuto lo stesso identico enigma durante il test dell'unità di un'app Android e la soluzione più semplice che mi è venuta è stata semplicemente quella di utilizzare Gson per convertire i miei oggetti con valore effettivo e previsto jsone confrontarli come stringhe.

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

assertEquals(expected, actual);

I vantaggi di questo corso confrontando manualmente campo per campo è che si confrontano tutti i vostri campi, quindi, anche se in seguito aggiungere un nuovo campo per la classe sarà ottenere testato automaticamente, rispetto a se si stesse utilizzando un mazzo di assertEquals()su tutti i campi, che dovrebbero poi essere aggiornati se aggiungi più campi alla tua classe.

jUnit mostra anche le stringhe in modo da poter vedere direttamente dove differiscono. Non sono sicuro di quanto sia affidabile l'ordinamento del campo Gson, potrebbe essere un potenziale problema.


1
L'ordine dei campi non è garantito da Gson. Potresti voler JsonParse le stringhe e confrontare i JsonElements risultanti dall'analisi
ozma

0

Ho provato tutte le risposte e niente ha funzionato davvero per me.

Quindi ho creato il mio metodo che confronta semplici oggetti java senza andare in profondità nelle strutture annidate ...

Il metodo restituisce null se tutti i campi corrispondono o una stringa contenente dettagli di mancata corrispondenza.

Vengono confrontate solo le proprietà che hanno un metodo getter.

Come usare

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

Output di esempio in caso di mancata corrispondenza

L'output mostra i nomi delle proprietà ei rispettivi valori degli oggetti confrontati

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

Codice (Java 8 e versioni successive):

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

0

In alternativa a junit-only, puoi impostare i campi su null prima dell'asserzione uguale a:

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

    assertEquals(expected, actual);

-1

Puoi inserire il codice di confronto che hai pubblicato in qualche metodo di utilità statica?

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

Quindi puoi usare questo metodo in JUnit in questo modo:

assertEquals ("Gli oggetti non sono uguali", "", findDifferences (obj1, obj));

che non è goffo e ti dà informazioni complete sulle differenze, se esistono (non esattamente nella forma normale di assertEqual ma ottieni tutte le informazioni quindi dovrebbe essere buono).


-1

Questo non aiuterà l'OP, ma potrebbe aiutare tutti gli sviluppatori C # che finiscono qui ...

Come ha scritto Enrique , dovresti sovrascrivere il metodo uguale.

Esiste una pratica migliore per ottenere ciò che voglio dall'oggetto, senza sottoclassare e sovrascrivere gli uguali (ugh)?

Il mio suggerimento è di non utilizzare una sottoclasse. Usa una lezione parziale.

Definizioni di classi parziali (MSDN)

Quindi la tua classe sembrerebbe ...

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

Per Java, concordo con il suggerimento di utilizzare la riflessione. Ricorda solo che dovresti evitare di usare la riflessione quando possibile. È lento, difficile da eseguire il debug e ancora più difficile da mantenere in futuro perché gli IDE potrebbero rompere il codice facendo una ridenominazione del campo o qualcosa del genere. Stai attento!


-1

Dai tuoi commenti ad altre risposte, non capisco cosa vuoi.

Solo per motivi di discussione, diciamo che la classe ha sovrascritto il metodo uguale.

Quindi il tuo UT avrà un aspetto simile a:

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

Assert.assertEquals(expected, actual). 

E hai finito. Inoltre, non è possibile ottenere il "quadro completo di uguaglianza" se l'affermazione fallisce.

Da quello che ho capito, stai dicendo che anche se il tipo sostituisse uguale a, non ti interesserebbe, poiché vuoi ottenere il "quadro completo di uguaglianza". Quindi non ha senso estendere e sovrascrivere gli uguali.

Quindi devi opzioni: o confrontare proprietà per proprietà, usando la riflessione o controlli hard-coded, io suggerirei quest'ultimo. Oppure: confronta le rappresentazioni leggibili dall'uomo di questi oggetti.

Ad esempio, è possibile creare una classe helper che serializzi il tipo che si desidera confrontare con un documento XML e quindi confrontare l'XML risultante! in questo caso, puoi vedere visivamente cosa è esattamente uguale e cosa non lo è.

Questo approccio ti darà l'opportunità di guardare il quadro completo, ma è anche relativamente ingombrante (e un po 'incline all'errore all'inizio).


È possibile che il mio termine "immagine della piena uguaglianza" sia confuso. L'implementazione di equals () risolverebbe effettivamente il problema. Mi interessa conoscere tutti i campi diseguali (rilevanti per l'uguaglianza) allo stesso tempo, senza dover rieseguire il test. Serializzare l'oggetto è un'altra possibilità, ma non ho necessariamente bisogno di un uguale profondo. Vorrei utilizzare le implementazioni equals () delle proprietà, se possibile.
Ryan Nelson,

Grande! Puoi assolutamente utilizzare gli uguali delle proprietà, come hai affermato nella tua domanda. Sembra che questa sia la soluzione più semplice in questo caso ma, come hai notato, il codice può essere molto sgradevole.
Vitaliy

-3

Puoi sovrascrivere il metodo uguale della classe come:

@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;
    }
}

Bene, se vuoi confrontare due oggetti di una classe devi implementare in qualche modo il metodo degli uguali e del codice hash


3
ma OP dice che non vuole ignorare gli uguali, vuole un modo migliore
Ankur
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.