Как мне утверждать равенство двух классов без метода равенства?

114

Скажем, у меня есть класс без метода equals (), для которого нет источника. Я хочу подтвердить равенство двух экземпляров этого класса.

Я могу сделать несколько утверждений:

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

Мне не нравится это решение, потому что я не получаю полной картины равенства, если раннее утверждение терпит неудачу.

Я могу вручную сравнить и отследить результат:

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

Это дает мне полную картину равенства, но неуклюже (и я даже не учел возможные нулевые проблемы). Третий вариант - использовать Comparator, но compareTo () не сообщает мне, для каких полей не удалось выполнить равенство.

Есть ли лучший способ получить то, что я хочу от объекта, без подкласса и переопределения равенства (тьфу)?

Райан Нельсон
источник
Вы ищете библиотеку, которая сделает за вас глубокое сравнение? как глубокие равные, предлагаемые на stackoverflow.com/questions/1449001/… ?
Викдор
3
Зачем нужно знать, почему два экземпляра не были равны. Обычно реализация equalметода только сообщает, равны ли два экземпляра, и нам все равно, почему значения не равны.
Bhesh Gurung
3
Я хочу знать, какие свойства неравны, чтобы исправить их. :)
Райан Нельсон
У всех Objectесть equalsметод, вы, вероятно, имели в виду отсутствие переопределенного метода равенства.
Стив Куо,
Лучший способ, который я могу придумать, - это использовать класс-оболочку или подкласс, а затем использовать его после переопределения метода equals ..
Thihara

Ответы:

67

Mockito предлагает сопоставление отражений:

Для последней версии Mockito используйте:

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

Для более старых версий используйте:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));
felvhage
источник
18
Этот класс находится в пакете org.mockito.internal.matchers.apachecommons. В документах Mockito указано: org.mockito.internal-> «Внутренние классы, не используемые клиентами». Используя это, вы подвергнете свой проект риску. Это может измениться в любой версии Mockito. Читайте здесь: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac
7
Mockito.refEq()Вместо этого используйте .
Джереми Као,
1
Mockito.refEq()терпит неудачу, если у объектов нет id set = (
cavpollo
1
@PiotrAleksanderChmielowski, извините, при работе с Spring + JPA + Entities объект Entity может иметь идентификатор (представляющий поле идентификатора таблицы базы данных), поэтому, когда он пуст (новый объект, еще не сохраненный в БД), refEqне работает для сравнения, поскольку метод хэш-кода не может сравнивать объекты.
cavpollo
3
Он работает нормально, но ожидаемое и фактическое находятся в неправильном порядке. Должно быть наоборот.
pkawiak 01
50

Здесь много правильных ответов, но я бы тоже хотел добавить свою версию. Это основано на Assertj.

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

public class TestClass {

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

ОБНОВЛЕНИЕ: в assertj v3.13.2 этот метод устарел, как указал Вудз в комментарии. Текущая рекомендация

public class TestClass {

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

}
Томас
источник
3
В assertj v3.13.2 этот метод устарел, и теперь рекомендуется использовать его usingRecursiveComparison()с isEqualTo(), так что строка будетassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz
46

Обычно я реализую этот вариант использования с помощью org.apache.commons.lang3.builder.EqualsBuilder.

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));
Абхиджит Куше
источник
1
Gradle: androidTestCompile 'org.apache.commons: Обще-lang3: 3,5'
Рул
1
Вы должны добавить это к вашему Gradle файл под «зависимости» , когда вы хотите использовать «org.apache.commons.lang3.builder.EqualsBuilder»
Roel
3
Это не дает намеков на то, какие именно поля на самом деле не совпадают.
Вадим
1
@Vadzim Я использовал приведенный ниже код, чтобы получить этот Assert.assertEquals (ReflectionToStringBuilder.toString (ожидается), ReflectionToStringBuilder.toString (фактический));
Abhijeet Kushe
2
Для этого требуется, чтобы все узлы в графе реализовали «равный» и «хэш-код», что в основном делает этот метод почти бесполезным. AssertJ isEqualToComparingFieldByFieldRecursively - это тот, который отлично работает в моем случае.
Джон Чжан
12

Я знаю, что он немного староват, но надеюсь, что это поможет.

Я столкнулся с той же проблемой, что и вы, поэтому после расследования я нашел несколько вопросов, похожих на этот, и, найдя решение, я отвечаю на них так же, поскольку думал, что это может помочь другим.

Ответ на этот аналогичный вопрос , получивший наибольшее количество голосов (а не выбранный автором) , является для вас наиболее подходящим решением.

По сути, он заключается в использовании библиотеки Unitils .

Это использование:

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

Что пройдет, даже если класс Userне будет реализован equals(). Вы можете увидеть больше примеров и действительно классное утверждение, названное assertLenientEqualsв их учебнике .

лопезвит
источник
1
К сожалению, Unitilsпохоже, он умер, см. Stackoverflow.com/questions/34658067/is-unitils-project-alive .
Кен Уильямс
8

Вы можете использовать Apache commons lang ReflectionToStringBuilder

Вы можете указать атрибуты, которые хотите протестировать, один за другим, или, лучше, исключить те, которые вам не нужны:

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

Затем вы сравниваете две строки как обычно. Что касается медленного отражения, я предполагаю, что это только для тестирования, поэтому не должно быть так важно.

Мэтью Фарвелл
источник
1
Дополнительным преимуществом этого подхода является то, что вы получаете визуальный вывод, отображающий ожидаемые и фактические значения в виде строк, исключая поля, которые вам не нужны.
fquinner
7

Если вы используете hamcrest для своих утверждений (assertThat) и не хотите использовать дополнительные тестовые библиотеки, то вы можете использовать его SamePropertyValuesAs.samePropertyValuesAsдля утверждения элементов, у которых нет переопределенного метода равенства.

Положительным моментом является то, что вам не нужно использовать еще одну тестовую среду, и она выдаст полезную ошибку, когда assert не работает ( expected: field=<value> but was field=<something else>), а не expected: true but was falseесли вы используете что-то вроде EqualsBuilder.reflectionEquals().

Обратной стороной является то, что это неглубокое сравнение и нет возможности для исключения полей (как в EqualsBuilder), поэтому вам придется работать с вложенными объектами (например, удалять их и сравнивать их независимо).

Лучший случай:

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

Уродливый случай:

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

Итак, выберите свой яд. Дополнительная структура (например, Unitils), бесполезная ошибка (например, EqualsBuilder) или неглубокое сравнение (hamcrest).

Шатер
источник
1
Для работы SamePropertyValues ​​Как вы должны добавить в проект зависимость hamcrest.org/JavaHamcrest/…
bigspawn
Мне нравится это решение, так как оно не добавляет «совершенно * другую дополнительную зависимость!»
GhostCat,
4

Библиотека Hamcrest 1.3 Utility Matchers имеет специальный сопоставитель, который использует отражение вместо равенства.

assertThat(obj1, reflectEquals(obj2));
Стефан Биркнер
источник
2

Некоторые методы сравнения отражений неглубокие

Другой вариант - преобразовать объект в json и сравнить строки.

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))
Авраам Шалев
источник
2

Используя Shazamcrest , вы можете:

assertThat(obj1, sameBeanAs(obj2));
Леонель Санчес да Силва
источник
Зачем использовать Shazamcrest вместо Hamcrest? stackoverflow.com/a/27817702/6648326
MasterJoe
1
@ MasterJoe2 В моих тестах reflectEqualsвозвращается, falseкогда объекты имеют разные ссылки.
Леонель Санчес да Силва,
1

Сравните поле за полем:

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
источник
1
Я не хочу этого делать, потому что ранний сбой утверждения скроет возможные сбои ниже.
Райан Нельсон
2
Извините, я пропустил эту часть вашего сообщения ... Почему "полная картина равенства" важна в среде модульного тестирования? Либо все поля равны (тест проходит), либо не все равны (тест не пройден).
EthanB
Я не хочу повторно запускать тест, чтобы определить, не равны ли другие поля. Я хочу заранее знать все поля, которые не равны, чтобы сразу обратиться к ним.
Райан Нельсон
1
Утверждение множества полей в одном тесте не будет считаться истинным «модульным» тестом. При традиционной разработке через тестирование (TDD) вы пишете небольшой тест, а затем достаточно кода, чтобы он прошел. Правильный способ сделать это - иметь по одному утверждению для каждого поля, просто не помещайте все утверждения в один тест. Создайте отдельный тест для каждого интересующего вас утверждения поля. Это позволит вам увидеть все ошибки во всех полях за один запуск пакета. Если это сложно, это, вероятно, означает, что ваш код изначально недостаточно модульный и, вероятно, его можно реорганизовать в более чистое решение.
Джесси Уэбб
Это определенно верное предложение, и это обычный способ решения нескольких утверждений в одном тесте. Единственная проблема здесь в том, что я хотел бы получить целостное представление об объекте. То есть я хотел бы протестировать все поля одновременно, чтобы убедиться, что объект находится в допустимом состоянии. Это несложно сделать, если у вас есть переопределенный метод equals () (которого, конечно, у меня нет в этом примере).
Райан Нельсон
1

Утверждения AssertJ могут использоваться для сравнения значений без #equalsправильного переопределения метода, например:

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

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);
Павел
источник
1

Поскольку это старый вопрос, я предложу другой современный подход с использованием JUnit 5.

Мне не нравится это решение, потому что я не получаю полной картины равенства, если раннее утверждение терпит неудачу.

В JUnit 5 есть вызываемый метод, Assertions.assertAll()который позволит вам сгруппировать все утверждения в вашем тесте вместе, и он будет выполнять каждое из них и выводить все неудачные утверждения в конце. Это означает, что любые утверждения, которые терпят неудачу первыми, не остановят выполнение последних утверждений.

assertAll("Test obj1 with obj2 equality",
    () -> assertEquals(obj1.getFieldA(), obj2.getFieldA()),
    () -> assertEquals(obj1.getFieldB(), obj2.getFieldB()),
    () -> assertEquals(obj1.getFieldC(), obj2.getFieldC()));
Patelb
источник
0

Вы можете использовать отражение, чтобы «автоматизировать» полное тестирование на равенство. вы можете реализовать код отслеживания равенства, который вы написали для одного поля, а затем использовать отражение для запуска этого теста для всех полей в объекте.

Jtahlborn
источник
0

Это общий метод сравнения, который сравнивает два объекта одного класса на предмет значений его полей (имейте в виду, что они доступны с помощью метода 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

Я наткнулся на очень похожий случай.

Я хотел сравнить в тесте, что объект имеет те же значения атрибутов, что и другой, но такие методы, как is(), refEq()и т. Д., Не будут работать по таким причинам, как мой объект, имеющий нулевое значение в своем idатрибуте.

Итак, это было решение, которое я нашел (ну, коллега нашел):

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

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

Если полученное значение reflectionCompareравно 0, это означает, что они равны. Если это -1 или 1, они различаются по какому-либо атрибуту.

Кавполло
источник
0

Та же самая загадка возникла при модульном тестировании приложения для Android, и самым простым решением, которое я придумал, было просто использовать Gson для преобразования моих фактических и ожидаемых объектов значений в jsonи сравнения их как строк.

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

assertEquals(expected, actual);

Преимущество этого по сравнению с ручным сравнением поля за полем заключается в том, что вы сравниваете все свои поля, поэтому даже если вы позже добавите новое поле в свой класс, оно будет автоматически протестировано, по сравнению с тем, если бы вы использовали кучуassertEquals() на все поля, которые затем необходимо будет обновить, если вы добавите дополнительные поля в свой класс.

jUnit также отображает строки для вас, чтобы вы могли сразу увидеть, где они различаются. Не уверен, насколько надежен порядок полей Gson, это может быть потенциальной проблемой.

Магнус В
источник
1
Gson не гарантирует порядок полей. Возможно, вы захотите выполнить JsonParse строк и сравнить элементы JsonElements, полученные в результате синтаксического анализа
ozma
0

Я перепробовал все ответы, и ничего не помогло.

Итак, я создал свой собственный метод, который сравнивает простые объекты Java, не углубляясь во вложенные структуры ...

Метод возвращает значение null, если все поля совпадают, или строка, содержащая сведения о несовпадении.

Сравниваются только свойства, у которых есть метод получения.

Как пользоваться

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

Пример вывода, если есть несоответствие

Вывод показывает имена свойств и соответствующие значения сравниваемых объектов.

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

Код (Java 8 и выше):

 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

В качестве альтернативы для junit-only вы можете установить для полей значение null до утверждения равенства:

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

    assertEquals(expected, actual);
Андрей Таран
источник
-1

Можете ли вы поместить опубликованный вами код сравнения в какой-нибудь статический служебный метод?

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

Тогда вы можете использовать этот метод в JUnit следующим образом:

assertEquals («Объекты не равны», «», findDifferences (obj1, obj));

который не является громоздким и дает вам полную информацию о различиях, если они существуют (через не совсем обычную форму assertEqual, но вы получаете всю информацию, поэтому она должна быть хорошей).

Камил
источник
-1

Это не поможет OP, но может помочь любым разработчикам C #, которые здесь окажутся ...

Как написал Энрике , вы должны переопределить метод equals.

Есть ли лучший способ получить то, что я хочу от объекта, без подкласса и переопределения равенства (тьфу)?

Мое предложение - не использовать подкласс. Используйте частичный класс.

Определения частичных классов (MSDN)

Итак, ваш класс будет выглядеть ...

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

Что касается Java, я бы согласился с предложением использовать отражение. Просто помните, что вам следует по возможности избегать использования отражения. Это медленно, сложно отлаживать и еще сложнее поддерживать в будущем, потому что IDE могут сломать ваш код, выполнив переименование поля или что-то в этом роде. Быть осторожен!

Джесси Уэбб
источник
-1

Из ваших комментариев к другим ответам я не понимаю, чего вы хотите.

Просто для обсуждения допустим, что класс действительно переопределил метод equals.

Итак, ваш UT будет выглядеть примерно так:

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

Assert.assertEquals(expected, actual). 

И все готово. Более того, вы не можете получить «полную картину равенства», если утверждение не выполняется.

Насколько я понимаю, вы говорите, что даже если бы тип переопределил equals, вас это не заинтересовало бы, поскольку вы хотите получить «полную картину равенства». Так что нет смысла расширять и отменять равные.

Итак, у вас есть варианты: либо сравнивать свойство по свойству, используя отражение или жестко запрограммированные проверки, я бы предложил последнее. Или: сравните удобочитаемые представления этих объектов.

Например, вы можете создать вспомогательный класс, который сериализует тип, который вы хотите сравнить, с XML-документом, а затем сравнивать полученный XML! в этом случае вы можете наглядно увидеть, что именно равно, а что нет.

Такой подход даст вам возможность увидеть полную картину, но он также относительно громоздок (и поначалу немного подвержен ошибкам).

Виталий
источник
Возможно, мой термин «картина полного равенства» сбивает с толку. Реализация equals () действительно решит проблему. Мне интересно знать все неравные поля (относящиеся к равенству) одновременно, без необходимости повторного запуска теста. Еще одна возможность - сериализация объекта, но мне не обязательно нужны глубокие равные. Я хотел бы по возможности использовать реализации свойств equals ().
Райан Нельсон
Большой! Вы абсолютно можете использовать те же свойства, которые вы указали в своем вопросе. Кажется, что это наиболее простое решение в данном случае, но, как вы заметили, код может быть очень неприятным.
Виталий
-3

Вы можете переопределить метод equals класса, например:

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

Что ж, если вы хотите сравнить два объекта из класса, вы должны каким-то образом реализовать метод equals и метод хэш-кода

Энрике Сан Мартин
источник
3
но OP говорит, что он не хочет отвергать равных, он хочет лучший способ
Анкур