¿Cómo afirmo la igualdad en dos clases sin un método igual?

111

Digamos que tengo una clase sin método equals (), para la cual no tengo la fuente. Quiero afirmar la igualdad en dos instancias de esa clase.

Puedo hacer varias afirmaciones:

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

No me gusta esta solución porque no obtengo la imagen de igualdad completa si falla una afirmación temprana.

Puedo comparar manualmente por mi cuenta y rastrear el resultado:

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

Esto me da una imagen de igualdad completa, pero es torpe (y ni siquiera he tenido en cuenta los posibles problemas de nulos). Una tercera opción es usar Comparator, pero compareTo () no me dirá qué campos fallaron en la igualdad.

¿Existe una mejor práctica para obtener lo que quiero del objeto, sin subclasificar y anular iguales (ugh)?

Ryan Nelson
fuente
¿Está buscando una biblioteca que haga una comparación profunda por usted? como deep-equals sugerido en stackoverflow.com/questions/1449001/… ?
Vikdor
3
¿Por qué necesita saber por qué las dos instancias no eran iguales? Por lo general, una implementación de equalmétodo solo dice si dos instancias son iguales y no nos importa por qué las intancias no son iguales.
Bhesh Gurung
3
Quiero saber qué propiedades son desiguales para poder arreglarlas. :)
Ryan Nelson
Todos los Objects tienen un equalsmétodo, probablemente quiso decir que ningún método es igual a anulado.
Steve Kuo
La mejor manera que se me ocurre es usar una clase contenedora o una subclase y luego usarla después de anular el método equals ..
Thihara

Respuestas:

66

Mockito ofrece un comparador de reflejos:

Para la última versión de Mockito use:

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

Para versiones anteriores, use:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));
felvhage
fuente
17
Esta clase está en paquete org.mockito.internal.matchers.apachecommons. Los documentos de Mockito dicen: org.mockito.internal-> "Clases internas, no para ser utilizadas por clientes". Pondrás tu proyecto en riesgo usando esto. Esto puede cambiar en cualquier versión de Mockito. Lea aquí: site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac
6
Úselo en su Mockito.refEq()lugar.
Jeremy Kao
1
Mockito.refEq()falla cuando los objetos no tienen un conjunto de identificación = (
cavpollo
1
@PiotrAleksanderChmielowski, lo siento, cuando se trabaja con Spring + JPA + Entities, el objeto Entity puede tener un id (que representa el campo id de la tabla de la base de datos), por lo que cuando está vacío (un nuevo objeto aún no almacenado en el DB), refEqfalla para comparar, ya que el método hashcode no puede comparar los objetos.
cavpollo
3
Funciona bien, pero lo esperado y lo real están en el orden incorrecto. Debería ser de otra manera.
pkawiak
48

Aquí hay muchas respuestas correctas, pero también me gustaría agregar mi versión. Esto se basa en Assertj.

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

public class TestClass {

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

ACTUALIZACIÓN: En assertj v3.13.2 este método está en desuso, como señaló Woodz en el comentario. La recomendación actual es

public class TestClass {

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

}
Thomas
fuente
3
En assertj v3.13.2 este método está en desuso y la recomendación es usar ahora usingRecursiveComparison()con isEqualTo(), de modo que la línea seaassertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz
45

Generalmente implemento este caso de uso usando org.apache.commons.lang3.builder.EqualsBuilder

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));
Abhijeet Kushe
fuente
1
Gradle: androidTestCompile 'org.apache.commons: commons-lang3: 3.5'
Roel
1
Debe agregar esto a su archivo gradle en "dependencias" cuando desee utilizar "org.apache.commons.lang3.builder.EqualsBuilder"
Roel
3
Esto no da ninguna pista sobre qué campos exactos no coincidieron en realidad.
Vadzim
1
@Vadzim He utilizado el siguiente código para obtener Assert.assertEquals (ReflectionToStringBuilder.toString (esperado), ReflectionToStringBuilder.toString (actual));
Abhijeet Kushe
2
Este requiere que todos los nodos en el gráfico implementen "igual" y "código hash", lo que básicamente hace que este método sea casi inútil. IsEqualToComparingFieldByFieldRecursively de AssertJ es el que funciona perfectamente en mi caso.
John Zhang
12

Sé que es un poco viejo, pero espero que ayude.

Me encontré con el mismo problema que tú, así que, después de investigar, encontré pocas preguntas similares a esta y, después de encontrar la solución, respondo lo mismo en esas, ya que pensé que podría ayudar a otros.

La respuesta más votada (no la elegida por el autor) de este pregunta similar , es la solución más adecuada para usted.

Básicamente, consiste en utilizar la biblioteca llamada Unitils .

Este es el uso:

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

Que pasará incluso si la clase Userno se implementa equals(). Puede ver más ejemplos y una afirmación realmente genial llamada assertLenientEqualsen su tutorial .

lopezvit
fuente
1
Desafortunadamente, Unitilsparece haber muerto, consulte stackoverflow.com/questions/34658067/is-unitils-project-alive .
Ken Williams
8

Puedes usar Apache commons lang ReflectionToStringBuilder

Puede especificar los atributos que desea probar uno por uno, o mejor, excluir aquellos que no desea:

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

Luego compara las dos cadenas de forma normal. En cuanto a que la reflexión es lenta, supongo que esto es solo para probar, por lo que no debería ser tan importante.

Matthew Farwell
fuente
1
El beneficio adicional de este enfoque es que obtiene una salida visual que muestra los valores esperados y reales como cadenas que excluyen los campos que no le interesan.
fquinner
7

Si está usando hamcrest para sus afirmaciones (assertThat) y no desea extraer bibliotecas de prueba adicionales, puede usar SamePropertyValuesAs.samePropertyValuesAs para afirmar elementos que no tienen un método igual anulado.

La ventaja es que no tiene que incorporar otro marco de prueba más y dará un error útil cuando la aserción falla ( expected: field=<value> but was field=<something else>) en lugar de expected: true but was falsesi usa algo como EqualsBuilder.reflectionEquals().

La desventaja es que es una comparación superficial y no hay opción para excluir campos (como en EqualsBuilder), por lo que tendrá que trabajar con objetos anidados (por ejemplo, eliminarlos y compararlos de forma independiente).

Mejor caso:

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

Caso feo:

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

Entonces, elige tu veneno. Marco adicional (por ejemplo, Unitils), error inútil (por ejemplo, EqualsBuilder) o comparación superficial (hamcrest).

Marquesina
fuente
1
Para trabajar SamePropertyValuesAs , debe agregar la dependencia del proyecto hamcrest.org/JavaHamcrest/…
bigspawn
Me gusta esta solución, ya que no agrega una dependencia adicional "completamente * diferente"
GhostCat
4

La biblioteca Hamcrest 1.3 Utility Matchers tiene un matcher especial que utiliza la reflexión en lugar de iguales.

assertThat(obj1, reflectEquals(obj2));
Stefan Birkner
fuente
2

Algunos de los métodos de comparación de reflexión son superficiales

Otra opción es convertir el objeto a json y comparar las cadenas.

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
fuente
2

Con Shazamcrest , puede hacer:

assertThat(obj1, sameBeanAs(obj2));
Leonel Sanches da Silva
fuente
¿Por qué usar shazamcrest en lugar de hamcrest? stackoverflow.com/a/27817702/6648326
MasterJoe
1
@ MasterJoe2 En mis pruebas, reflectEqualsdevuelve falsecuando los objetos tienen referencias diferentes.
Leonel Sanches da Silva
1

Comparar campo por 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);
EthanB
fuente
1
Esto es algo que no quiero hacer, porque una falla de aserción temprana ocultará posibles fallas debajo.
Ryan Nelson
2
Lo siento, me perdí esa parte de tu publicación ... ¿Por qué es importante una "imagen de igualdad total" en un entorno de prueba unitaria? O los campos son todos iguales (prueba aprobada) o no todos son iguales (prueba falla).
EthanB
No quiero tener que volver a ejecutar la prueba para descubrir si otros campos no son iguales. Quiero saber de antemano todos los campos que son desiguales para poder abordarlos de inmediato.
Ryan Nelson
1
Afirmar muchos campos en una prueba no se consideraría una verdadera prueba de "unidad". Con el desarrollo tradicional basado en pruebas (TDD), escribe una pequeña prueba y luego solo el código suficiente para que pase. Tener una afirmación por campo es la forma correcta de hacerlo, pero no ponga todas las afirmaciones en una sola prueba. Cree una prueba diferente para cada afirmación de campo que le interese. Esto le permitirá ver todos los errores con todos los campos con una sola ejecución de la suite. Si esto es difícil, probablemente signifique que su código no es lo suficientemente modular en primer lugar y probablemente se puede refactorizar en una solución más limpia.
Jesse Webb
Esta es definitivamente una sugerencia válida, y es la forma habitual en la que resolvería múltiples afirmaciones en una sola prueba. El único desafío aquí es que me gustaría tener una vista holística del objeto. Es decir, me gustaría probar todos los campos simultáneamente para verificar que el objeto esté en un estado válido. Esto no es difícil de hacer cuando tiene un método equals () anulado (que por supuesto no tengo en este ejemplo).
Ryan Nelson
1

Las afirmaciones de AssertJ se pueden usar para comparar los valores sin que el #equalsmétodo se anule correctamente, por ejemplo:

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

// ...

assertThat(actual)
    .usingRecursiveComparison()
    .isEqualTo(expected);
Pavel
fuente
1

Dado que esta pregunta es antigua, sugeriré otro enfoque moderno utilizando JUnit 5.

No me gusta esta solución porque no obtengo la imagen de igualdad completa si falla una afirmación temprana.

Con JUnit 5, hay un método llamado Assertions.assertAll()que le permitirá agrupar todas las aserciones en su prueba y ejecutará cada una y generará las aserciones fallidas al final. Esto significa que cualquier afirmación que falle primero no detendrá la ejecución de las últimas afirmaciones.

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

Puede utilizar la reflexión para "automatizar" la prueba de igualdad total. puede implementar el código de "seguimiento" de igualdad que escribió para un solo campo, luego usar la reflexión para ejecutar esa prueba en todos los campos del objeto.

jtahlborn
fuente
0

Este es un método de comparación genérico, que compara dos objetos de una misma clase por los valores de sus campos (tenga en cuenta que se puede acceder a ellos mediante el método 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
fuente
0

Me encontré con un caso muy similar.

Quería comparar en una prueba de que un objeto tiene los mismos valores de atributos como la otra, pero los métodos como is(), refEq(), etc, no haría el trabajo por razones como mi objeto que tiene un valor nulo en suid atributo.

Así que esta fue la solución que encontré (bueno, un compañero de trabajo encontró):

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

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

Si el valor obtenido de reflectionComparees 0, significa que son iguales. Si es -1 o 1, difieren en algún atributo.

cavpollo
fuente
0

Tenía exactamente el mismo acertijo cuando probaba unitariamente una aplicación de Android, y la solución más fácil que se me ocurrió fue simplemente usar Gson para convertir mis objetos de valor real y esperado jsony compararlos como cadenas.

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

assertEquals(expected, actual);

Las ventajas de esto sobre la comparación manual campo por campo es que usted compara todos sus campos, por lo que incluso si luego agrega un nuevo campo a su clase, se probará automáticamente, en comparación con si estuviera usando un montón deassertEquals() de todos los campos, que luego deberían actualizarse si agrega más campos a su clase.

jUnit también muestra las cadenas para que pueda ver directamente en qué se diferencian. Sin Gsonembargo, no estoy seguro de cuán confiable sea el pedido de campo , eso podría ser un problema potencial.

Magnus W
fuente
1
Gson no garantiza el orden de los campos. Es posible que desee JsonParse las cadenas y comparar los JsonElements resultantes del análisis sintáctico
ozma
0

Probé todas las respuestas y nada realmente funcionó para mí.

Así que he creado mi propio método que compara objetos java simples sin profundizar en estructuras anidadas ...

El método devuelve un valor nulo si todos los campos coinciden o la cadena contiene detalles que no coinciden.

Solo se comparan las propiedades que tienen un método getter.

Cómo utilizar

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

Salida de muestra si hay una discrepancia

La salida muestra los nombres de las propiedades y los valores respectivos de los objetos comparados

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

Código (Java 8 y superior):

 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
fuente
0

Como alternativa para junit-only, puede establecer los campos en nulos antes de la aserción igual:

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

    assertEquals(expected, actual);
Andrew Taran
fuente
-1

¿Puede poner el código de comparación que publicó en algún método de utilidad estática?

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

Entonces puedes usar este método en JUnit así:

assertEquals ("Los objetos no son iguales", "", findDifferences (obj1, obj));

que no es torpe y le brinda información completa sobre las diferencias, si existen (a través de no exactamente en la forma normal de assertEqual, pero obtiene toda la información, por lo que debería ser bueno).

Kamil
fuente
-1

Esto no ayudará al OP, pero podría ayudar a cualquier desarrollador de C # que termine aquí ...

Como publicó Enrique , debes anular el método de iguales.

¿Existe una mejor práctica para obtener lo que quiero del objeto, sin subclasificar y anular iguales (ugh)?

Mi sugerencia es no utilizar una subclase. Utilice una clase parcial.

Definiciones de clases parciales (MSDN)

Entonces tu clase se vería así ...

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

Para Java, estaría de acuerdo con la sugerencia de usar la reflexión. Solo recuerda que debes evitar usar la reflexión siempre que sea posible. Es lento, difícil de depurar e incluso más difícil de mantener en el futuro porque los IDE podrían romper su código al cambiar el nombre de un campo o algo así. ¡Ten cuidado!

Jesse Webb
fuente
-1

De sus comentarios a otras respuestas, no entiendo lo que quiere.

Solo por el bien de la discusión, digamos que la clase anuló el método de igualdad.

Entonces su UT se verá algo como:

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

Assert.assertEquals(expected, actual). 

Y ya está. Además, no puede obtener la "imagen de igualdad total" si la afirmación falla.

Por lo que tengo entendido, está diciendo que incluso si el tipo anulara los iguales, no estaría interesado en él, ya que desea obtener la "imagen de igualdad total". Por lo tanto, tampoco tiene sentido extender y anular iguales.

Entonces tiene opciones: o comparar propiedad por propiedad, usando la reflexión o verificaciones codificadas, sugeriría lo último. O: compare representaciones legibles por humanos de estos objetos.

Por ejemplo, puede crear una clase auxiliar que serialice el tipo que desea comparar con un documento XML y luego comparar el XML resultante. en este caso, puede ver visualmente qué es exactamente igual y qué no.

Este enfoque le dará la oportunidad de ver la imagen completa, pero también es relativamente engorroso (y un poco propenso a errores al principio).

Vitalidad
fuente
Es posible que mi término "imagen de igualdad total" sea confuso. La implementación de equals () resolvería el problema. Me interesa conocer todos los campos desiguales (relevantes para la igualdad) al mismo tiempo, sin tener que volver a ejecutar la prueba. Serializar el objeto es otra posibilidad, pero no necesito necesariamente un igual profundo. Me gustaría utilizar las implementaciones equals () de las propiedades si es posible.
Ryan Nelson
¡Excelente! Absolutamente puede utilizar los iguales de las propiedades, como indicó en su pregunta. Parece que esta es la solución más sencilla en este caso, pero como notó, el código puede ser muy desagradable.
Vitaliy
-3

Puede anular el método equals de la clase como:

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

Bueno, si desea comparar dos objetos de una clase, debe implementar de alguna manera el método equals y el código hash

Enrique San Martín
fuente
3
pero OP dice que no quiere anular iguales, quiere una mejor manera
Ankur