Як я можу стверджувати рівність у двох класах без методу рівних?


111

Скажіть, у мене є клас, що не має методу 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);

Це дає мені повну картину рівності, але незграбна (і я навіть не враховувала можливих нульових проблем). Третім варіантом є використання компаратора, але параметрTo () не скаже мені, у яких полях не було рівності.

Чи є краща практика отримати те, що я хочу від об'єкта, без підкласифікації та переосмислення рівних (тьфу)?


Ви шукаєте бібліотеку, яка глибоке порівняння для вас? як глибокі рівні, запропоновані на сайті stackoverflow.com/questions/1449001/… ?
Вікдор

3
Чому потрібно знати, чому два екземпляри не були рівними. Зазвичай реалізація equalметоду говорить лише про те, що два екземпляри рівні, і нас не хвилює, чому ідентичні дані не рівні.
Bhesh Gurung

3
Я хочу знати, які властивості неоднакові, щоб я міг їх виправити. :)
Райан Нельсон

У всіх Objectє equalsметод, ви, мабуть, мали на увазі відсутність методу, що переосмислюється, дорівнює
Стів Куо

Найкращий спосіб, який я можу придумати, - це використовувати обгортковий клас або підклас, а потім використовувати його після зміни методу рівних ..
Тіхара

Відповіді:


66

Mockito пропонує рефлексію:

Для останньої версії Mockito використовуйте:

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

Для старих версій використовуйте:

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

17
Цей клас є в упаковці org.mockito.internal.matchers.apachecommons. Документи Mockito визначають: org.mockito.internal-> "Внутрішні класи, які не повинні використовуватися клієнтами." Використовуючи це, ви піддасте свій проект ризику. Це може змінитись у будь-якій версії Mockito. Читайте тут: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac

6
Використовуйте Mockito.refEq()замість цього.
Джеремі Као

1
Mockito.refEq()не вдається, коли для об'єктів не встановлено ідентифікатор = (
cavpollo

1
@PiotrAleksanderChmielowski, вибачте, під час роботи з Spring + JPA + Entities об'єкт Entity може мати ідентифікатор (представляє поле ідентифікатора таблиці бази даних), тому коли він порожній (новий об'єкт ще не зберігається в БД), refEqвиходить з ладу для порівняння, оскільки метод хеш-коду не в змозі порівняти об'єкти.
cavpollo

3
Це добре працює, але очікувані та фактичні в неправильному порядку. Це повинно бути навпаки.
pkawiak

48

Тут є багато правильних відповідей, але я також хотів би додати свою версію. На цьому базується 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

45

Я зазвичай реалізую цей настрій за допомогою org.apache.commons.lang3.builder.EqualsBuilder

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

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

1
Вам потрібно додати це до файлу gradle в розділі "залежності", коли ви хочете використовувати "org.apache.commons.lang3.builder.EqualsBuilder"
Roel

3
Це не дає ніякої підказки щодо того, які саме поля фактично не відповідали.
Вадим

1
@Vadzim Я використовував наведений нижче код, щоб отримати цей Assert.assertEquals (ReflectionToStringBuilder.toString (очікуваний), ReflectionToStringBuilder.toString (фактичний));
Abhijeet Kushe

2
Для цього потрібно, щоб усі вузли в графі реалізували "рівний" та "хеш-код", що в основному робить цей метод майже марним. У моїй справі ідеально працює те, що в моєму випадку працює ідеально рівнийToComparingFieldByFieldRecursively.
Джон Чжан

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 для затвердження елементів, у яких немає методу переопределення рівних.

Переваги полягають у тому, що вам не доведеться втягувати ще одну тестову рамку, і це стане корисною помилкою, коли збірка не працює ( 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, як ви повинні додати в залежність
bigspawn

Мені подобається це рішення, оскільки воно не додає "абсолютно * іншої додаткової залежності!"
GhostCat


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


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

1
Це те, що я не хочу робити, тому що недолік раннього твердження приховає можливі збої нижче.
Райан Нельсон

2
Вибачте, я пропустив цю частину вашої публікації ... Чому в середовищі тестування одиниць важлива "картина повної рівності"? Або всі поля рівні (тестові проходи), або вони не всі рівні (тест не вдається).
EthanB

Мені не хочеться повторно проводити тест, щоб виявити, чи інші поля не рівні. Я хочу знати наперед всі нерівні поля, щоб я міг їх вирішити відразу.
Райан Нельсон

1
Ствердження багатьох полів за один тест не вважатиметься справжнім тестом "одиниці". З традиційною тестовою розробкою (TDD) ви пишете невеликий тест, а потім лише достатньо коду, щоб пройти його. Маючи одне твердження на поле - це правильний спосіб зробити це, просто не ставте всі твердження в один тест. Створіть інший тест для кожного твердження про поле, яке вас цікавить. Це дозволить вам побачити всі помилки з усіма полями за один запуск пакета. Якщо це важко, це, швидше за все, означає, що ваш код в першу чергу недостатньо модульний, і, швидше за все, його можна перетворити на більш чистий розчин.
Джессі Вебб

Це, безумовно, дійсна пропозиція, і це звичайний спосіб вирішити кілька тверджень в одному тесті. Єдине завдання тут - я хотів би цілісний погляд на об'єкт. Тобто я хотів би перевірити всі поля одночасно, щоб перевірити, чи об’єкт у дійсному стані. Це не важко зробити, коли у вас є метод переосмисленого рівняння () (що, звичайно, у мене в цьому прикладі немає).
Райан Нельсон

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

0

Ви можете використовувати відображення для "автоматизації" повного тестування рівності. ви можете реалізувати код "відстеження рівності", який ви написали для одного поля, а потім використовувати відображення для запуску цього тесту на всіх полях об'єкта.


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

Як альтернатива лише для липня, ви можете встановити поля на нуль до рівня твердження:

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

який не є незграбним і дає вам повну інформацію про відмінності, якщо вони існують (через не зовсім у звичайній формі assrtEqual, але ви отримуєте всю інформацію, так що це має бути добре).


-1

Це не допоможе ОП, але це може допомогти будь-яким розробникам C #, які опиняються тут ...

Як і Енріке, розміщений , вам слід перекрити метод рівних.

Чи є краща практика отримати те, що я хочу від об'єкта, без підкласифікації та переосмислення рівних (тьфу)?

Моя пропозиція - не використовувати підклас. Використовуйте частковий клас.

Визначення часткових класів (MSDN)

Так ваш клас виглядав би так ...

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

Що стосується Java, я погодився б із пропозицією використовувати рефлексію. Просто пам’ятайте, що вам слід уникати використання роздумів, коли це можливо. Це повільно, важко налагоджувати, а ще важче підтримувати в майбутньому, тому що IDE можуть зламати ваш код, перейменувавши поле або щось подібне. Будь обережний!


-1

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

Просто заради обговорення, скажемо, що клас переосмислив метод рівних.

Тож ваш UT буде виглядати приблизно так:

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

Assert.assertEquals(expected, actual). 

І ви закінчили. Більше того, ви не зможете отримати «повну картину рівності», якщо твердження не вдасться.

З того, що я розумію, ви говорите, що навіть якби тип переосмислив рівний рівень, ви не були б зацікавлені в цьому, оскільки ви хочете отримати "повну картину рівності". Тому немає жодного сенсу розширювати і переосмислювати рівне.

Тож вам доведеться запропонувати варіанти: або порівняти властивість за властивостями, використовуючи відображення або жорстко закодовані чеки, я б запропонував останнє. Або: порівняйте людські уявлення цих об'єктів.

Наприклад, ви можете створити допоміжний клас, який серіалізує тип, який ви хочете порівняти з XML-документом, і порівняти отриманий XML! у цьому випадку можна візуально побачити, що саме дорівнює, а що ні.

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


Можливо, мій термін "повна картина рівності" заплутаний. Реалізація рівняння () дійсно вирішить проблему. Мені цікаво одночасно знати всі нерівні поля (відповідні рівності), без необхідності повторного запуску тесту. Серіалізація об'єкта - це ще одна можливість, але я не обов'язково потребую глибоких рівних. Я хотів би використовувати можливості (equals) реалізації властивостей, якщо це можливо.
Райан Нельсон

Чудово! Ви абсолютно можете використовувати рівні властивості, як ви заявили у своєму запитанні. Здається, що це найбільш просте рішення в цьому випадку, але, як ви зазначили, код може бути дуже неприємним.
Віталій

-3

Ви можете перекрити метод рівності класу, як:

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

Ну, якщо ви хочете порівняти два об'єкти з класу, ви повинні якось реалізувати рівний і метод хеш-коду


3
але ОП каже, що він не хоче переборювати рівних, він хоче кращого шляху
Анкур
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.