Bagaimana cara menegaskan persamaan di dua kelas tanpa metode yang sama?


111

Katakanlah saya memiliki kelas tanpa metode sama dengan (), yang tidak memiliki sumber. Saya ingin menegaskan kesetaraan pada dua contoh kelas itu.

Saya bisa melakukan banyak pernyataan:

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

Saya tidak suka solusi ini karena saya tidak mendapatkan gambaran kesetaraan penuh jika pernyataan awal gagal.

Saya dapat membandingkan sendiri secara manual dan melacak hasilnya:

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

Ini memberi saya gambaran kesetaraan penuh, tetapi kikuk (dan saya bahkan belum memperhitungkan kemungkinan masalah nol). Opsi ketiga adalah menggunakan Comparator, tetapi bandingkanTo () tidak akan memberi tahu saya bidang mana yang gagal persamaan.

Apakah ada praktik yang lebih baik untuk mendapatkan apa yang saya inginkan dari objek, tanpa subclassing dan overridding sama (ugh)?


Apakah Anda mencari perpustakaan yang melakukan perbandingan mendalam untuk Anda? seperti persamaan dalam yang disarankan di stackoverflow.com/questions/1449001/… ?
Vikdor

3
Mengapa Anda perlu tahu mengapa kedua contoh itu tidak sama. Biasanya, implementasi equalmetode hanya memberi tahu apakah dua contoh sama, dan kami tidak peduli mengapa intes tidak sama.
Bhesh Gurung

3
Saya ingin tahu properti apa yang tidak sama sehingga saya bisa memperbaikinya. :)
Ryan Nelson

Semua Objectmemiliki equalsmetode, yang mungkin Anda maksud tidak ada metode sama yang diganti.
Steve Kuo

Cara terbaik yang dapat saya pikirkan adalah dengan menggunakan kelas pembungkus atau subkelas dan kemudian menggunakannya setelah mengganti metode yang sama ..
Thihara

Jawaban:


66

Mockito menawarkan pencocok refleksi:

Untuk versi terbaru penggunaan Mockito:

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

Untuk versi yang lebih lama gunakan:

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

17
Kelas ini ada dalam paket org.mockito.internal.matchers.apachecommons. Status dokumen Mockito: org.mockito.internal-> "Kelas internal, tidak untuk digunakan oleh klien." Anda akan membahayakan proyek Anda menggunakan ini. Ini dapat berubah di versi Mockito apa pun. Baca di sini: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac

6
Gunakan Mockito.refEq()sebagai gantinya.
Jeremy Kao

1
Mockito.refEq()gagal ketika objek tidak memiliki id set = (
cavpollo

1
@PiotrAleksanderChmielowski, maaf, saat bekerja dengan Spring + JPA + Entities, objek Entity mungkin memiliki id (mewakili field id dari tabel database), jadi ketika kosong (objek baru belum disimpan di DB), refEqgagal untuk membandingkan karena metode kode hash tidak dapat membandingkan objek.
cavpollo

3
Ini berfungsi dengan baik, tetapi yang diharapkan dan aktual berada dalam urutan yang salah. Seharusnya sebaliknya.
pkawiak

48

Ada banyak jawaban yang benar di sini, tetapi saya ingin menambahkan versi saya juga. Ini didasarkan pada Assertj.

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

public class TestClass {

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

PEMBARUAN: Dalam assertj v3.13.2 metode ini tidak digunakan lagi seperti yang ditunjukkan oleh Woodz dalam komentar. Rekomendasi saat ini adalah

public class TestClass {

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

}

3
Dalam assertj v3.13.2 metode ini tidak digunakan lagi dan rekomendasinya sekarang digunakan usingRecursiveComparison()dengan isEqualTo(), sehingga barisnya adalahassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz

45

Saya biasanya menerapkan usecase ini menggunakan org.apache.commons.lang3.builder.EqualsBuilder

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

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

1
Anda perlu menambahkan ini ke file gradle di bawah "dependencies" jika Anda ingin menggunakan "org.apache.commons.lang3.builder.EqualsBuilder"
Roel

3
Ini tidak memberikan petunjuk apa pun tentang bidang yang sebenarnya tidak cocok.
Vadzim

1
@Vadzim Saya telah menggunakan kode di bawah ini untuk mendapatkan Assert.assertEquals (ReflectionToStringBuilder.toString (diharapkan), ReflectionToStringBuilder.toString (aktual));
Abhijeet Kushe

2
Yang satu ini mengharuskan semua node dalam grafik menerapkan "equal" dan "hashcode", yang pada dasarnya membuat metode ini hampir tidak berguna. IsEqualToComparingFieldByFieldRecursively AssertJ adalah yang bekerja dengan sempurna dalam kasus saya.
John Zhang

12

Saya tahu ini agak tua, tapi saya harap ini membantu.

Saya mengalami masalah yang sama dengan Anda, jadi, setelah penyelidikan, saya menemukan beberapa pertanyaan serupa dari yang ini, dan, setelah menemukan solusinya, saya menjawab hal yang sama, karena saya pikir itu bisa membantu orang lain.

Jawaban yang paling banyak dipilih (bukan yang dipilih oleh penulis) untuk pertanyaan serupa ini , adalah solusi yang paling sesuai untuk Anda.

Pada dasarnya, ini terdiri dari penggunaan perpustakaan yang disebut Unitils .

Inilah gunanya:

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

Yang akan lolos bahkan jika kelas Usertidak diimplementasikan equals(). Anda dapat melihat lebih banyak contoh dan pernyataan yang sangat keren disebut assertLenientEqualsdalam tutorial mereka .


1
Sayangnya Unitilstampaknya telah mati, lihat stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams

8

Anda dapat menggunakan Apache commons lang ReflectionToStringBuilder

Anda dapat menentukan atribut yang ingin Anda uji satu per satu, atau lebih baik, mengecualikan yang tidak Anda inginkan:

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

Anda kemudian membandingkan kedua string tersebut seperti biasa. Untuk poin tentang refleksi yang lambat, saya berasumsi ini hanya untuk pengujian, jadi seharusnya tidak terlalu penting.


1
Manfaat tambahan dari pendekatan ini adalah Anda mendapatkan keluaran visual yang menampilkan nilai yang diharapkan dan sebenarnya sebagai string tidak termasuk bidang yang tidak Anda pedulikan.
fquinner

7

Jika Anda menggunakan hamcrest untuk asserts Anda (assertThat) dan tidak ingin menggunakan libs uji tambahan, Anda dapat menggunakan SamePropertyValuesAs.samePropertyValuesAs untuk menegaskan item yang tidak memiliki metode sama yang diganti.

Keuntungannya adalah Anda tidak perlu menggunakan framework pengujian lain dan ini akan memberikan kesalahan yang berguna saat assert gagal ( expected: field=<value> but was field=<something else>) daripada expected: true but was falsejika Anda menggunakan sesuatu seperti EqualsBuilder.reflectionEquals().

Kelemahannya adalah ini adalah perbandingan yang dangkal dan tidak ada opsi untuk mengecualikan bidang (seperti yang ada di EqualsBuilder), jadi Anda harus bekerja di sekitar objek bersarang (misalnya menghapusnya dan membandingkannya secara independen).

Kasus terbaik:

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

Kasus Jelek:

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

Jadi, pilih racunmu. Kerangka tambahan (mis. Unitils), kesalahan tidak membantu (mis. EqualsBuilder), atau perbandingan dangkal (hamcrest).


1
Untuk pekerjaan SamePropertyValuesAs Anda harus menambahkan dalam proyek dependecy hamcrest.org/JavaHamcrest/…
bigspawn

Saya suka solusi ini, karena tidak menambahkan "ketergantungan tambahan yang sepenuhnya * berbeda!
GhostCat


2

Beberapa metode perbandingan refleksi dangkal

Pilihan lainnya adalah mengubah objek menjadi json dan membandingkan string.

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

Bandingkan bidang demi bidang:

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
Ini adalah sesuatu yang tidak ingin saya lakukan, karena kegagalan penegasan awal akan menyembunyikan kemungkinan kegagalan di bawah.
Ryan Nelson

2
Maaf, saya melewatkan bagian postingan Anda itu ... Mengapa "gambaran kesetaraan penuh" penting dalam lingkungan pengujian unit? Entah semua bidang sama (uji lulus), atau tidak semuanya sama (uji gagal).
EthanB

Saya tidak ingin menjalankan ulang pengujian untuk mengetahui apakah bidang lain tidak sama. Saya ingin mengetahui terlebih dahulu semua bidang yang tidak setara sehingga saya dapat mengatasinya sekaligus.
Ryan Nelson

1
Menegaskan banyak bidang dalam satu pengujian tidak akan dianggap sebagai pengujian 'unit' yang sebenarnya. Dengan pengembangan berbasis pengujian tradisional (TDD), Anda menulis pengujian kecil, lalu hanya cukup kode untuk membuatnya lulus. Memiliki satu pernyataan per bidang adalah cara yang benar untuk melakukannya, namun jangan lakukan semua pernyataan ke dalam satu pengujian. Buat pengujian berbeda untuk setiap pernyataan bidang yang Anda minati. Ini akan memungkinkan Anda untuk melihat semua kesalahan dengan semua bidang dengan menjalankan suite tunggal. Jika ini sulit, kemungkinan besar kode Anda tidak cukup modular sejak awal dan kemungkinan dapat diubah menjadi solusi yang lebih bersih.
Jesse Webb

Ini jelas merupakan saran yang valid, dan merupakan cara biasa Anda menyelesaikan beberapa pernyataan dalam satu pengujian. Satu-satunya tantangan di sini adalah, saya ingin melihat objek secara holistik. Artinya, saya ingin menguji semua bidang secara bersamaan untuk memverifikasi bahwa objek dalam keadaan valid. Ini tidak sulit dilakukan jika Anda memiliki metode equals () yang diganti (yang tentu saja tidak saya miliki dalam contoh ini).
Ryan Nelson

1

Penegasan AssertJ dapat digunakan untuk membandingkan nilai tanpa #equalsmetode yang diganti dengan benar, misalnya:

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

// ...

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

1

Karena pertanyaan ini sudah lama, saya akan menyarankan pendekatan modern lainnya menggunakan JUnit 5.

Saya tidak suka solusi ini karena saya tidak mendapatkan gambaran kesetaraan penuh jika pernyataan awal gagal.

Dengan JUnit 5, ada sebuah metode yang dipanggil Assertions.assertAll()yang akan memungkinkan Anda untuk mengelompokkan semua pernyataan dalam pengujian Anda bersama-sama dan itu akan mengeksekusi masing-masing dan mengeluarkan pernyataan yang gagal di akhir. Ini berarti bahwa pernyataan apa pun yang gagal lebih dulu tidak akan menghentikan eksekusi pernyataan terakhir.

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

0

Anda dapat menggunakan refleksi untuk "mengotomatiskan" pengujian kesetaraan penuh. Anda dapat menerapkan kode "pelacakan" persamaan yang Anda tulis untuk satu bidang, kemudian menggunakan refleksi untuk menjalankan pengujian tersebut pada semua bidang di objek.


0

Ini adalah metode perbandingan umum, yang membandingkan dua objek dari kelas yang sama untuk nilai-nilainya dari bidangnya (ingatlah bahwa itu dapat diakses dengan metode 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

Saya menemukan kasus yang sangat mirip.

Saya ingin membandingkan pada tes bahwa obyek memiliki nilai atribut yang sama dengan satu sama lain, tetapi metode seperti is(), refEq(), dll tidak akan bekerja untuk alasan seperti objek saya memiliki nilai null dalam Surat idatribut.

Jadi, inilah solusi yang saya temukan (yah, rekan kerja menemukan):

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

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

Jika nilai yang diperoleh reflectionCompareadalah 0 berarti sama. Jika -1 atau 1, mereka berbeda pada beberapa atribut.



0

Saya memiliki teka-teki yang sama persis saat menguji unit aplikasi Android, dan solusi termudah yang saya dapatkan adalah menggunakan Gson untuk mengonversi objek nilai aktual dan yang diharapkan menjadi jsondan membandingkannya sebagai string.

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

assertEquals(expected, actual);

Keuntungan dari ini dibandingkan secara manual membandingkan bidang per bidang adalah Anda membandingkan semua bidang Anda, jadi meskipun Anda nanti menambahkan bidang baru ke kelas Anda itu akan diuji secara otomatis, dibandingkan dengan jika Anda menggunakan banyakassertEquals() pada semua bidang, yang kemudian perlu diperbarui jika Anda menambahkan lebih banyak bidang ke kelas Anda.

jUnit juga menampilkan string untuk Anda sehingga Anda dapat langsung melihat perbedaannya. Tidak yakin seberapa andal pemesanan lapangan Gson, itu bisa menjadi masalah potensial.


1
Urutan kolom tidak dijamin oleh Gson. Anda mungkin ingin JsonParse string dan membandingkan JsonElements yang dihasilkan dari parsing
ozma

0

Saya mencoba semua jawaban dan tidak ada yang berhasil untuk saya.

Jadi saya telah membuat metode saya sendiri yang membandingkan objek java sederhana tanpa masuk jauh ke dalam struktur bersarang ...

Metode mengembalikan nol jika semua bidang cocok atau string berisi detail yang tidak cocok.

Hanya properti yang memiliki metode pengambil yang dibandingkan.

Cara Penggunaan

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

Contoh keluaran jika ada ketidakcocokan

Output menunjukkan nama properti dan nilai masing-masing dari objek yang dibandingkan

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

Kode (Java 8 dan lebih tinggi):

 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

Sebagai alternatif untuk junit-only, Anda dapat menyetel kolom ke null sebelum sama dengan pernyataan:

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

    assertEquals(expected, actual);

-1

Dapatkah Anda memasukkan kode perbandingan yang Anda posting ke dalam beberapa metode utilitas statis?

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

Kemudian Anda dapat menggunakan metode ini di JUnit seperti ini:

assertEquals ("Objek tidak sama", "", findDifferences (obj1, obj));

yang tidak kikuk dan memberi Anda informasi lengkap tentang perbedaan, jika ada (tidak persis dalam bentuk assertEqual normal tetapi Anda mendapatkan semua info sehingga seharusnya bagus).


-1

Ini tidak akan membantu OP, tetapi mungkin membantu pengembang C # yang berakhir di sini ...

Seperti yang diposting Enrique , Anda harus mengganti metode sama dengan.

Apakah ada praktik yang lebih baik untuk mendapatkan apa yang saya inginkan dari objek, tanpa subclassing dan overridding sama (ugh)?

Saran saya adalah tidak menggunakan subclass. Gunakan kelas parsial.

Partial Class Definitions (MSDN)

Jadi kelas Anda akan terlihat seperti ...

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

Untuk Java, saya setuju dengan saran untuk menggunakan refleksi. Ingatlah bahwa Anda harus menghindari penggunaan refleksi bila memungkinkan. Proses ini lambat, sulit untuk di-debug, dan bahkan lebih sulit untuk dipertahankan di masa mendatang karena IDE dapat merusak kode Anda dengan melakukan penggantian nama bidang atau semacamnya. Hati-hati!


-1

Dari komentar Anda hingga jawaban lain, saya tidak mengerti apa yang Anda inginkan.

Hanya demi diskusi, katakanlah bahwa kelas tersebut mengganti metode sama dengan.

Jadi UT Anda akan terlihat seperti ini:

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

Assert.assertEquals(expected, actual). 

Dan Anda selesai. Selain itu, Anda tidak bisa mendapatkan "gambaran kesetaraan penuh" jika pernyataan gagal.

Dari apa yang saya pahami, Anda mengatakan bahwa meskipun jenisnya menimpa sama, Anda tidak akan tertarik padanya, karena Anda ingin mendapatkan "gambaran kesetaraan penuh". Jadi tidak ada gunanya memperpanjang dan menimpa yang sama.

Jadi, Anda harus memilih: membandingkan properti dengan properti, menggunakan refleksi atau pemeriksaan hard-code, saya sarankan yang terakhir. Atau: bandingkan representasi yang dapat dibaca manusia dari objek-objek ini.

Misalnya, Anda bisa membuat kelas helper yang membuat serial jenis yang ingin Anda bandingkan dengan dokumen XML dan kemudian membandingkan XML yang dihasilkan! dalam hal ini, Anda dapat melihat secara visual apa yang sebenarnya sama dan apa yang tidak.

Pendekatan ini akan memberi Anda kesempatan untuk melihat gambaran lengkapnya tetapi juga relatif rumit (dan pada awalnya sedikit rawan kesalahan).


Mungkin istilah saya "gambar kesetaraan penuh" membingungkan. Menerapkan equals () memang akan menyelesaikan masalah. Saya tertarik untuk mengetahui semua bidang yang tidak sama (relevan dengan kesetaraan) pada saat yang sama, tanpa harus menjalankan ulang pengujian. Serialisasi objek adalah kemungkinan lain, tetapi saya tidak perlu persamaan yang mendalam. Saya ingin menggunakan implementasi equals () dari properti jika memungkinkan.
Ryan Nelson

Bagus! Anda benar-benar dapat menggunakan properti yang sama, seperti yang Anda nyatakan dalam pertanyaan Anda. Tampaknya ini adalah solusi paling mudah dalam kasus ini, tetapi seperti yang Anda catat, kodenya bisa sangat buruk.
Vitaliy

-3

Anda dapat mengganti metode sama dengan kelas seperti:

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

Nah, jika Anda ingin membandingkan dua objek dari kelas, Anda harus menerapkan metode kode hash dan sama dengan cara tertentu


3
tetapi OP mengatakan bahwa dia tidak ingin menimpa yang sederajat, dia menginginkan cara yang lebih baik
Ankur
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.