¿Cómo debería una unidad probar el contrato hashCode-equals?

79

En pocas palabras, el contrato hashCode, según el object.hashCode () de Java:

  1. El código hash no debería cambiar a menos que algo que afecte a equals () cambie
  2. equals () implica que los códigos hash son ==

Supongamos que se interesa principalmente en los objetos de datos inmutables: su información nunca cambia después de que se construyen, por lo que se supone que el n. Eso deja el número 2: el problema es simplemente confirmar que igual implica código hash ==.

Obviamente, no podemos probar todos los objetos de datos imaginables a menos que ese conjunto sea trivialmente pequeño. Entonces, ¿cuál es la mejor manera de escribir una prueba unitaria que probablemente detecte los casos comunes?

Dado que las instancias de esta clase son inmutables, hay formas limitadas de construir tal objeto; esta prueba unitaria debe cubrir todos ellos si es posible. En la parte superior de mi cabeza, los puntos de entrada son los constructores, la deserialización y los constructores de subclases (que deberían ser reducibles al problema de llamada al constructor).

[Voy a intentar responder a mi propia pregunta mediante la investigación. La entrada de otros StackOverflowers es un mecanismo de seguridad bienvenido para este proceso.]

[Esto podría aplicarse a otros idiomas de OO, así que agrego esa etiqueta].

Paul Brinkley
fuente
1
Por lo general, encuentro que el contrato ya se rompió debido a la implementación de equals o hashcode, pero no a ambos. Allí openpojo ayuda en un proyecto de Java. EqualsAndHashCodeMatchRule ayuda con eso. Las respuestas ya existentes proporcionan suficientes detalles sobre la prueba del resto del contrato.
Michiel Leegwater

Respuestas:

73

EqualsVerifier es un proyecto de código abierto relativamente nuevo y hace un muy buen trabajo probando el contrato de igualdad. No tiene los problemas que tiene EqualsTester de GSBase. Sin duda lo recomendaría.

Benno Richters
fuente
7
¡Solo usar EqualsVerifier me enseñó algo sobre Java!
David
¿Estoy loco? ¡EqualsVerifier no parecía comprobar la semántica del objeto de valor en absoluto! Piensa que mi clase implementa correctamente equals (), pero mi clase usa el comportamiento predeterminado "==". ¡¿Cómo puede ser esto?! Debo estar perdiendo algo simple.
JB Rainsberger
¡Ajá! Si no anulo equals (), ¿EqualsVerifier asume que todo está bien? Eso no es lo que esperaría. Esperaría el mismo comportamiento para no anular equals () que para anular equals () para hacer exactamente lo mismo super.equals (). Extraño.
JB Rainsberger
7
@JBRainsberger ¡Hola! Creador de EqualsVerifier aquí. Lamento que lo encuentre confuso. Re: no anular es igual a: puede habilitar su comportamiento esperado con "allFieldsShouldBeUsed ()". Este será el predeterminado en la próxima versión, por las razones que usted indica. Re: instancias idénticas: no creo que pueda ayudarte sin ver un código de ejemplo. ¿Puede dar más detalles en una nueva pregunta o en la lista de correo de EV en groups.google.com/forum/?fromgroups#!forum/equalsverifier ? ¡Gracias!
jqno
1
EqualsVerifier es realmente poderoso. Gané una gran cantidad de tiempo al no probar cada combinación potencial de objetos no iguales. Los mensajes de error son realmente precisos e instructivos. Y el verificador es muy configurable si su convención de codificación es diferente de la expectativa predeterminada. Buen trabajo @jqno
gontard
11

Mi consejo sería pensar en por qué / cómo esto podría no ser cierto, y luego escribir algunas pruebas unitarias que se dirijan a esas situaciones.

Por ejemplo, digamos que tienes una Setclase personalizada . Dos conjuntos son iguales si contienen los mismos elementos, pero es posible que las estructuras de datos subyacentes de dos conjuntos iguales difieran si esos elementos se almacenan en un orden diferente. Por ejemplo:

MySet s1 = new MySet( new String[]{"Hello", "World"} );
MySet s2 = new MySet( new String[]{"World", "Hello"} );
assertEquals(s1, s2);
assertTrue( s1.hashCode()==s2.hashCode() );

En este caso, el orden de los elementos en los conjuntos puede afectar su hash, según el algoritmo de hash que haya implementado. Entonces, este es el tipo de prueba que escribiría, ya que prueba el caso en el que sé que sería posible que algún algoritmo de hash produzca resultados diferentes para dos objetos que he definido como iguales.

Debería utilizar un estándar similar con su propia clase personalizada, sea lo que sea.

Eli Courtwright
fuente
6

Vale la pena usar los complementos de junit para esto. Consulte la clase EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ . Puede extender esto e implementar createInstance y createNotEqualInstance, esto verificará que los métodos equals y hashCode sean correctos.


fuente
4

Recomendaría EqualsTester de GSBase. Hace básicamente lo que quieres. Sin embargo, tengo dos problemas (menores) con él:

  • El constructor hace todo el trabajo, lo que no considero una buena práctica.
  • Falla cuando una instancia de clase A equivale a una instancia de una subclase de clase A. Esto no es necesariamente una violación del contrato de igualdad.
Benno Richters
fuente
2

[En el momento de escribir este artículo, se publicaron otras tres respuestas].

Para reiterar, el objetivo de mi pregunta es encontrar casos estándar de pruebas para confirmar que hashCodey equalsestán de acuerdo entre sí. Mi enfoque para esta pregunta es imaginar los caminos comunes que toman los programadores al escribir las clases en cuestión, es decir, datos inmutables. Por ejemplo:

  1. Escribió equals()sin escribir hashCode(). Esto a menudo significa que la igualdad se definió como la igualdad de los campos de dos instancias.
  2. Escribió hashCode()sin escribir equals(). Esto puede significar que el programador buscaba un algoritmo hash más eficiente.

En el caso del # 2, el problema me parece inexistente. No se han realizado instancias adicionales equals(), por lo que no se requieren instancias adicionales para tener códigos hash iguales. En el peor de los casos, el algoritmo hash puede producir un rendimiento más deficiente para los mapas hash, lo que está fuera del alcance de esta pregunta.

En el caso del # 1, la prueba unitaria estándar implica la creación de dos instancias del mismo objeto con los mismos datos pasados ​​al constructor y la verificación de códigos hash iguales. ¿Qué pasa con los falsos positivos? Es posible elegir los parámetros del constructor que dan como resultado códigos hash iguales en un algoritmo que, sin embargo, no es sólido. Una prueba unitaria que tiende a evitar tales parámetros cumpliría el espíritu de esta pregunta. El atajo aquí es inspeccionar el código fuente equals(), pensar detenidamente y escribir una prueba basada en eso, pero si bien esto puede ser necesario en algunos casos, también puede haber pruebas comunes que detectan problemas comunes, y tales pruebas también cumplen con el espíritu de esta pregunta.

Por ejemplo, si la clase que se va a probar (llamémosla Datos) tiene un constructor que toma un String, y las instancias construidas a partir de Strings son equals()instancias que lo fueron equals(), entonces una buena prueba probablemente probaría:

  • new Data("foo")
  • otro new Data("foo")

Incluso podríamos verificar el código hash para new Data(new String("foo"))obligar a la cadena a no ser internada, aunque es más probable que produzca un código hash correcto que Data.equals()un resultado correcto, en mi opinión.

La respuesta de Eli Courtwright es un ejemplo de pensar mucho en una forma de romper el algoritmo hash basado en el conocimiento de la equalsespecificación. El ejemplo de una colección especial es bueno, ya Collectionque los mensajes de correo electrónico creados por el usuario aparecen a veces y son bastante propensos a fallar en el algoritmo hash.

Paul Brinkley
fuente
1

Este es uno de los únicos casos en los que tendría múltiples afirmaciones en una prueba. Dado que necesita probar el método equals, también debe verificar el método hashCode al mismo tiempo. Entonces, en cada uno de sus casos de prueba de métodos iguales, verifique también el contrato hashCode.

A one = new A(...);
A two = new A(...);
assertEquals("These should be equal", one, two);
int oneCode = one.hashCode();
assertEquals("HashCodes should be equal", oneCode, two.hashCode());
assertEquals("HashCode should not change", oneCode, one.hashCode());

Y, por supuesto, comprobar si hay un buen código hash es otro ejercicio. Honestamente, no me molestaría en hacer una doble verificación para asegurarme de que el hashCode no cambiara en la misma ejecución, ese tipo de problema se maneja mejor al detectarlo en una revisión de código y ayudar al desarrollador a comprender por qué esa no es una buena manera para escribir métodos hashCode.

Rob Spieldenner
fuente
0

Si tengo una clase Thing, como la mayoría de las demás, escribo una clase ThingTest, que contiene todas las pruebas unitarias para esa clase. Cada uno ThingTesttiene un método

 public static void checkInvariants(final Thing thing) {
    ...
 }

y si la Thingclase anula hashCode y es igual, tiene un método

 public static void checkInvariants(final Thing thing1, Thing thing2) {
    ObjectTest.checkInvariants(thing1, thing2);
    ... invariants that are specific to Thing
 }

Ese método es responsable de verificar todos los invariantes que están diseñados para mantenerse entre cualquier par de Thingobjetos. El ObjectTestmétodo al que delega es responsable de verificar todos los invariantes que deben mantenerse entre cualquier par de objetos. Como equalsy hashCodeson métodos de todos los objetos, ese método comprueba que hashCodey equalsson coherentes.

Luego tengo algunos métodos de prueba que crean pares de Thingobjetos y los pasan al checkInvariantsmétodo por pares . Utilizo la partición de equivalencia para decidir qué pares vale la pena probar. Por lo general, creo que cada par sea diferente en un solo atributo, más una prueba que prueba dos objetos equivalentes.

A veces también tengo un checkInvariantsmétodo de 3 argumentos , aunque encuentro que es menos útil para encontrar defectos, así que no hago esto a menudo

Raedwald
fuente
Esto es similar a lo que menciona otro póster aquí: stackoverflow.com/a/190989/545127
Raedwald