Jak stwierdzić równość w dwóch klasach bez metody równości?

114

Powiedzmy, że mam klasę bez metody equals (), do której nie ma źródła. Chcę zapewnić równość w dwóch instancjach tej klasy.

Mogę wykonać wiele potwierdzeń:

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

Nie podoba mi się to rozwiązanie, ponieważ nie mam pełnego obrazu równości, jeśli wczesne potwierdzenie zawodzi.

Mogę samodzielnie samodzielnie porównać i śledzić wynik:

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

Daje mi to pełny obraz równości, ale jest niezgrabny (i nawet nie uwzględniłem możliwych zerowych problemów). Trzecią opcją jest użycie komparatora, ale metoda compareTo () nie powie mi, które pola nie osiągnęły równości.

Czy jest lepsza praktyka, aby uzyskać to, czego chcę od obiektu, bez podklas i nadpisywania równych (ugh)?

Ryan Nelson
źródło
Szukasz biblioteki, która dokona dla Ciebie dokładnych porównań? jak deep-equals sugerowani na stackoverflow.com/questions/1449001/… ?
Vikdor
3
Dlaczego musisz wiedzieć, dlaczego te dwa wystąpienia nie były równe. Zazwyczaj implementacja equalmetody mówi tylko, czy dwa wystąpienia są równe i nie obchodzi nas, dlaczego intencje nie są równe.
Bhesh Gurung
3
Chcę wiedzieć, jakie właściwości są nierówne, żeby móc je naprawić. :)
Ryan Nelson
Wszystkie Objectmają swoją equalsmetodę, prawdopodobnie miałeś na myśli brak metody zastępowania równych wartości.
Steve Kuo
Najlepszym sposobem, o jakim
przychodzi mi do głowy,

Odpowiedzi:

67

Mockito oferuje dopasowanie do refleksji:

Do najnowszej wersji Mockito użyj:

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

W przypadku starszych wersji użyj:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));
felvhage
źródło
18
Ta klasa jest w pakiecie org.mockito.internal.matchers.apachecommons. Stan dokumentacji Mockito: org.mockito.internal-> "Klasy wewnętrzne, których nie mogą używać klienci." Używając tego, narazisz swój projekt na ryzyko. Może się to zmienić w każdej wersji Mockito. Przeczytaj tutaj: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac
7
Użyj Mockito.refEq()zamiast tego.
Jeremy Kao
1
Mockito.refEq()kończy się niepowodzeniem, gdy obiekty nie mają ustawionego identyfikatora = (
cavpollo
1
@PiotrAleksanderChmielowski, przepraszam, pracując z Spring + JPA + Entities, obiekt Entity może mieć id (reprezentujący pole id tabeli bazy danych), więc gdy jest pusty (nowy obiekt nie jest jeszcze zapisany w DB), refEqzawodzi do porównania, ponieważ metoda hashcode nie może porównać obiektów.
cavpollo
3
Działa dobrze, ale oczekiwane i rzeczywiste są w złej kolejności. Powinno być na odwrót.
pkawiak
50

Jest tu wiele poprawnych odpowiedzi, ale chciałbym też dodać moją wersję. Jest to oparte na Assertj.

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

public class TestClass {

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

AKTUALIZACJA: W assertj w wersji 3.13.2 ta metoda jest przestarzała, jak wskazał Woodz w komentarzu. Aktualne zalecenie to

public class TestClass {

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

}
Tomasz
źródło
3
W assertj w wersji 3.13.2 ta metoda jest przestarzała, a zalecenie jest teraz używane usingRecursiveComparison()z isEqualTo(), tak że wiersz toassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz
46

Generalnie implementuję ten przypadek użycia przy użyciu org.apache.commons.lang3.builder.EqualsBuilder

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));
Abhijeet Kushe
źródło
1
Gradle: androidTestCompile 'org.apache.commons: commons-lang3: 3.5'
Roel
1
Musisz to dodać do swojego pliku gradle w sekcji „zależności”, jeśli chcesz używać „org.apache.commons.lang3.builder.EqualsBuilder”
Roel
3
Nie daje to żadnej wskazówki, które dokładnie pola w rzeczywistości się nie zgadzają.
Vadzim
1
@Vadzim Użyłem poniższego kodu, aby uzyskać Assert.assertEquals (ReflectionToStringBuilder.toString (oczekiwane), ReflectionToStringBuilder.toString (rzeczywiste));
Abhijeet Kushe
2
To wymaga, aby wszystkie węzły w grafie implementowały „równe” i „kod skrótu”, co w zasadzie czyni tę metodę prawie bezużyteczną. AssertJ's isEqualToComparingFieldByFieldRecursively jest tym, który działa idealnie w moim przypadku.
John Zhang
12

Wiem, że jest trochę stary, ale mam nadzieję, że pomoże.

Mam ten sam problem, co ty, więc po zbadaniu znalazłem kilka podobnych pytań niż to, a po znalezieniu rozwiązania odpowiadam na te same, ponieważ pomyślałem, że może pomóc innym.

Najczęściej głosowana odpowiedź (nie wybrana przez autora) na to podobne pytanie jest dla Ciebie najbardziej odpowiednim rozwiązaniem.

Zasadniczo polega na wykorzystaniu biblioteki o nazwie Unitils .

To jest zastosowanie:

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

Co przejdzie, nawet jeśli klasa Usernie jest implementowana equals(). Możesz zobaczyć więcej przykładów i naprawdę fajną asercję nazwaną assertLenientEqualsw ich samouczku .

lopezvit
źródło
1
Niestety Unitilswygląda na to, że nie żyje, zobacz stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams,
8

Możesz użyć Apache commons lang ReflectionToStringBuilder

Możesz określić atrybuty, które chcesz przetestować, pojedynczo lub, lepiej, wykluczyć te, których nie chcesz:

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

Następnie porównujesz te dwa ciągi w normalny sposób. Jeśli chodzi o powolne odbicie, zakładam, że jest to tylko do testowania, więc nie powinno być tak ważne.

Matthew Farwell
źródło
1
Dodatkową zaletą tego podejścia jest to, że otrzymujesz wizualne dane wyjściowe przedstawiające oczekiwane i rzeczywiste wartości jako ciągi znaków z wyłączeniem pól, które nie są dla Ciebie ważne.
fquinner
7

Jeśli używasz hamcrest do swoich potwierdzeń (assertThat) i nie chcesz pobierać dodatkowych bibliotek testowych, możesz użyć SamePropertyValuesAs.samePropertyValuesAs do potwierdzenia elementów, które nie mają nadpisanej metody równości.

Zaletą jest to, że nie musisz pobierać kolejnego frameworka testowego i da użyteczny błąd, gdy assert zawiedzie ( expected: field=<value> but was field=<something else>), zamiast expected: true but was falseużywać czegoś podobnego EqualsBuilder.reflectionEquals().

Wadą jest to, że jest to płytkie porównanie i nie ma opcji wykluczania pól (tak jak w EqualsBuilder), więc będziesz musiał obejść zagnieżdżone obiekty (np. Usunąć je i porównać niezależnie).

Najlepszy przypadek:

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

Brzydka sprawa:

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

Więc wybierz swoją truciznę. Dodatkowe ramy (np. Unitils), niepomocny błąd (np. EqualsBuilder) lub płytkie porównanie (hamcrest).

Duży namiot
źródło
1
Do pracy SamePropertyValuesAs musisz dodać w zależności od projektu hamcrest.org/JavaHamcrest/ ...
bigspawn
Podoba mi się to rozwiązanie, ponieważ nie dodaje „zupełnie * innej dodatkowej zależności!”
GhostCat,
2

Niektóre metody porównania odbić są płytkie

Inną opcją jest przekonwertowanie obiektu na json i porównanie ciągów.

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
źródło
2

Korzystając z Shazamcrest , możesz:

assertThat(obj1, sameBeanAs(obj2));
Leonel Sanches da Silva
źródło
Po co używać shazamcrest zamiast hamcrest? stackoverflow.com/a/27817702/6648326
MasterJoe
1
@ MasterJoe2 W moich testach reflectEqualszwraca, falsegdy obiekty mają różne odniesienia.
Leonel Sanches da Silva
1

Porównaj pole po polu:

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
źródło
1
To jest coś, czego nie chcę robić, ponieważ wczesna awaria asertu ukryje poniżej możliwe awarie.
Ryan Nelson
2
Przepraszam, przegapiłem tę część Twojego posta ... Dlaczego „pełny obraz równości” jest ważny w środowisku testów jednostkowych? Albo wszystkie pola są równe (test przechodzi), albo nie wszystkie są równe (test kończy się niepowodzeniem).
EthanB
Nie chcę ponownie uruchamiać testu, aby sprawdzić, czy inne pola nie są równe. Chcę z góry znać wszystkie pola, które są nierówne, aby móc od razu się nimi zająć.
Ryan Nelson
1
Twierdzenie wielu pól w jednym teście nie byłoby uważane za prawdziwy test „jednostkowy”. W przypadku tradycyjnego programowania opartego na testach (TDD) piszesz mały test, a następnie wystarczy kod, aby go przejść. Posiadanie jednego potwierdzenia na pole to właściwy sposób, po prostu nie umieszczaj wszystkich potwierdzeń w jednym teście. Utwórz inny test dla każdego potwierdzenia pola, na którym Ci zależy. Umożliwi to wyświetlenie wszystkich błędów we wszystkich polach przy jednym uruchomieniu pakietu. Jeśli jest to trudne, prawdopodobnie oznacza to, że Twój kod nie jest wystarczająco modułowy i prawdopodobnie może zostać przekształcony w czystsze rozwiązanie.
Jesse Webb
Jest to zdecydowanie słuszna sugestia i jest to zwykły sposób rozwiązywania wielu twierdzeń w jednym teście. Jedyne wyzwanie polega na tym, że chciałbym mieć całościowe spojrzenie na przedmiot. Oznacza to, że chciałbym przetestować wszystkie pola jednocześnie, aby sprawdzić, czy obiekt jest w prawidłowym stanie. Nie jest to trudne, gdy masz nadpisaną metodę equals () (której oczywiście nie mam w tym przykładzie).
Ryan Nelson
1

Asercje AssertJ mogą służyć do porównywania wartości bez #equalspoprawnie przesłanianej metody, np .:

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

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);
Pavel
źródło
1

Ponieważ to pytanie jest stare, zasugeruję inne nowoczesne podejście z wykorzystaniem JUnit 5.

Nie podoba mi się to rozwiązanie, ponieważ nie mam pełnego obrazu równości, jeśli wczesne potwierdzenie zawodzi.

W JUnit 5 istnieje metoda wywoływana, Assertions.assertAll()która pozwoli ci zgrupować wszystkie asercje w twoim teście i wykona każde z nich i wyprowadzi wszystkie nieudane asercje na końcu. Oznacza to, że wszelkie asercje, które zawiodą jako pierwsze, nie zatrzymają wykonywania ostatnich twierdzeń.

assertAll("Test obj1 with obj2 equality",
    () -> assertEquals(obj1.getFieldA(), obj2.getFieldA()),
    () -> assertEquals(obj1.getFieldB(), obj2.getFieldB()),
    () -> assertEquals(obj1.getFieldC(), obj2.getFieldC()));
patelb
źródło
0

Możesz użyć refleksji, aby „zautomatyzować” pełne testowanie równości. możesz zaimplementować kod „śledzenia” równości napisany dla pojedynczego pola, a następnie użyć odbicia, aby uruchomić ten test na wszystkich polach w obiekcie.

jtahlborn
źródło
0

Jest to ogólna metoda porównania, która porównuje dwa obiekty tej samej klasy pod względem wartości jej pól (pamiętaj, że te są dostępne za pomocą metody 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
źródło
0

Natknąłem się na bardzo podobny przypadek.

Chciałem porównać na teście, że obiekt miał te same wartości atrybutów jak innym, ale podobne metody is(), refEq()itp nie będzie działać z powodów podobnych do mojego obiektu o wartości zerowej w swojejid atrybut.

Więc to było rozwiązanie, które znalazłem (cóż, współpracownik znalazł):

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

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

Jeśli wartość uzyskana z reflectionComparewynosi 0, oznacza to, że są równe. Jeśli jest -1 lub 1, różnią się one w pewnym atrybucie.

cavpollo
źródło
0

Dokładnie tę samą zagadkę napotkałem podczas testowania jednostkowego aplikacji na Androida, a najłatwiejszym rozwiązaniem, które wymyśliłem, było po prostu użycie Gson do konwersji moich obiektów wartości rzeczywistej i oczekiwanej na jsoni porównania ich jako ciągów.

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

assertEquals(expected, actual);

Zaletą tego w porównaniu z ręcznym porównywaniem pola po polu jest to, że porównujesz wszystkie pola, więc nawet jeśli później dodasz nowe pole do swojej klasy, zostanie ono automatycznie przetestowane w porównaniu do sytuacji, gdy używałeś wielu pólassertEquals() on wszystkie pola, które musiałyby zostać zaktualizowane, jeśli dodasz więcej pól do swojej klasy.

jUnit wyświetla również ciągi znaków, dzięki czemu możesz bezpośrednio zobaczyć, gdzie się różnią. Nie jestem jednak pewien, jak niezawodne jest porządkowanie pól Gson, może to być potencjalny problem.

Magnus W
źródło
1
Kolejność pól nie jest gwarantowana przez firmę Gson. Możesz chcieć JsonParse ciągi i porównać JsonElements wynikające z parsowania
ozma
0

Wypróbowałem wszystkie odpowiedzi i nic tak naprawdę nie działało.

Dlatego stworzyłem własną metodę, która porównuje proste obiekty Java bez wchodzenia w zagnieżdżone struktury ...

Metoda zwraca wartość null, jeśli wszystkie pola są zgodne lub ciąg zawiera szczegóły niezgodności.

Porównywane są tylko właściwości, które mają metodę pobierającą.

Jak używać

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

Przykładowe dane wyjściowe w przypadku niezgodności

Dane wyjściowe pokazują nazwy właściwości i odpowiednie wartości porównywanych obiektów

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

Kod (Java 8 i nowsze):

 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
źródło
0

Jako alternatywę dla tylko junit możesz ustawić pola na null przed potwierdzeniem równości:

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

    assertEquals(expected, actual);
Andrew Taran
źródło
-1

Czy możesz umieścić opublikowany kod porównawczy w jakiejś statycznej metodzie narzędziowej?

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

Niż możesz użyć tej metody w JUnit w następujący sposób:

assertEquals ("Obiekty nie są równe", "", findDifferences (obj1, obj));

który nie jest niezgrabny i daje pełne informacje o różnicach, jeśli one istnieją (poprzez nie do końca w normalnej formie assertEqual, ale otrzymujesz wszystkie informacje, więc powinno być dobrze).

Kamil
źródło
-1

To nie pomoże OP, ale może pomóc każdemu deweloperowi C #, który skończy tutaj ...

Jak opublikował Enrique , powinieneś zastąpić metodę equals.

Czy jest lepsza praktyka, aby uzyskać to, czego chcę od obiektu, bez podklas i nadpisywania równych (ugh)?

Moja sugestia jest taka, aby nie używać podklasy. Użyj częściowej klasy.

Częściowe definicje klas (MSDN)

Twoja klasa wyglądałaby jak ...

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

W przypadku Javy zgodziłbym się z sugestią użycia refleksji. Pamiętaj tylko, że powinieneś unikać refleksji, kiedy tylko jest to możliwe. Jest powolny, trudny do debugowania, a nawet trudniejszy do utrzymania w przyszłości, ponieważ IDE mogą złamać twój kod, zmieniając nazwę pola lub coś w tym rodzaju. Bądź ostrożny!

Jesse Webb
źródło
-1

Od twoich komentarzy po inne odpowiedzi, nie rozumiem, czego chcesz.

Na potrzeby dyskusji powiedzmy, że klasa przesłała metodę equals.

Więc twój UT będzie wyglądał mniej więcej tak:

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

Assert.assertEquals(expected, actual). 

Gotowe. Ponadto nie można uzyskać „pełnego obrazu równości”, jeśli stwierdzenie nie powiedzie się.

Z tego, co rozumiem, mówisz, że nawet gdyby typ zastąpił równe, nie byłbyś nim zainteresowany, ponieważ chcesz uzyskać „pełny obraz równości”. Nie ma więc sensu rozszerzać i zastępować równych sobie.

Masz więc opcje: albo porównaj właściwość po właściwości, używając odbicia lub zakodowanych testów, sugerowałbym to drugie. Lub: porównaj reprezentacje tych obiektów w postaci czytelnej dla człowieka .

Na przykład możesz utworzyć klasę pomocniczą, która serializuje typ, który chcesz porównać z dokumentem XML, a następnie porównać wynikowy XML! w tym przypadku możesz wizualnie zobaczyć, co dokładnie jest równe, a co nie.

Takie podejście daje możliwość spojrzenia na pełny obraz, ale jest również stosunkowo uciążliwe (i początkowo jest mało podatne na błędy).

Vitaliy
źródło
Możliwe, że mój termin „pełny obraz równości” jest mylący. Wdrożenie equals () rzeczywiście rozwiązałoby problem. Interesuje mnie poznanie wszystkich nierównych pól (związanych z równością) w tym samym czasie, bez konieczności ponownego uruchamiania testu. Serializacja obiektu to kolejna możliwość, ale niekoniecznie potrzebuję głębokiego równości. Chciałbym, jeśli to możliwe, wykorzystać implementacje equals () właściwości.
Ryan Nelson
Wspaniały! Jak powiedziałeś w swoim pytaniu, możesz bezwzględnie wykorzystać te same właściwości. Wydaje się, że jest to najprostsze rozwiązanie w tym przypadku, ale jak zauważyłeś, kod może być bardzo nieprzyjemny.
Vitaliy
-3

Możesz nadpisać metodę equals klasy, na przykład:

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

Cóż, jeśli chcesz porównać dwa obiekty z klasy, musisz w jakiś sposób zaimplementować metodę equals i metodę hash code

Enrique San Martín
źródło
3
ale OP mówi, że nie chce zastępować równych sobie, chce lepszego sposobu
Ankur