java - para - ¿Cómo debería una unidad probar el contrato hashCode-igual?
metodo hash java (8)
En pocas palabras, el contrato hashCode, según el objeto object.hashCode () de Java:
- El código hash no debería cambiar a menos que algo que afecte a equals () cambie
- equals () implica que los códigos hash son ==
Asumamos interés principalmente en objetos de datos inmutables: su información nunca cambia una vez que se han construido, por lo que se supone que el valor n. ° 1 es válido. Eso deja el # 2: el problema es simplemente uno de confirmar que igual implica un código hash ==.
Obviamente, no podemos probar todos los objetos de datos concebibles a menos que ese conjunto sea trivialmente pequeño. Entonces, ¿cuál es la mejor manera de escribir una prueba unitaria que probablemente atrape los casos comunes?
Como las instancias de esta clase son inmutables, existen formas limitadas de construir tal objeto; esta prueba unitaria debe cubrir todos ellos si es posible. Fuera de mi cabeza, los puntos de entrada son los constructores, la deserialización y los constructores de subclases (que deberían poder reducirse al problema de llamada del constructor).
[Voy a tratar de responder mi propia pregunta a través de la investigación. La entrada de otros StackOverflowers es un mecanismo de seguridad bienvenido para este proceso.]
[Esto podría ser aplicable a otros lenguajes OO, así que estoy agregando esa etiqueta.]
Este es uno de los únicos casos en los que tendría múltiples aseveraciones en una prueba. Como necesita probar el método equals, también debe verificar el método hashCode al mismo tiempo. Por lo tanto, en cada uno de los casos de prueba del método equals, compruebe 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, buscar un buen hashCode es otro ejercicio. Honestamente, no me molestaría en hacer doble comprobación para asegurarme de que el hashCode no cambiara en la misma ejecución, ese tipo de problema se maneja mejor al capturarlo en una revisión de código y ayudar al desarrollador a entender por qué no es una buena manera para escribir métodos hashCode.
Mi consejo sería pensar por qué / cómo esto alguna vez podría no ser cierto, y luego escribir algunas pruebas unitarias que se dirijan a esas situaciones.
Por ejemplo, supongamos que tiene una clase de Set
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 hash que hayas implementado. Así que este es el tipo de prueba que escribiría, ya que prueba el caso donde sé que sería posible que un algoritmo hash produzca resultados diferentes para dos objetos que he definido para que sean iguales.
Debería usar un estándar similar con su propia clase personalizada, sea lo que sea.
Recomendaría el EqualsTester de GSBase. Básicamente, lo que quieres. Sin embargo, tengo dos problemas menores:
- El constructor hace todo el trabajo, que no considero una buena práctica.
- Falla cuando una instancia de clase A es igual a una instancia de una subclase de clase A. Esto no es necesariamente una violación del contrato igual.
Si tengo una Thing
clase, como la mayoría de las demás hago, escribo una ThingTest
clase de ThingTest
, que contiene todas las pruebas de unidad para esa clase. Cada ThingTest
tiene un método
public static void checkInvariants(final Thing thing) {
...
}
y si la clase Thing
anula hashCode y es igual a que 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 todas las invariantes que están diseñadas para contener entre cualquier par de objetos Thing
. El método ObjectTest
que delega es responsable de verificar todas las invariantes que deben contenerse entre cualquier par de objetos. Como equals
y hashCode
son métodos de todos los objetos, ese método comprueba que hashCode
e equals
son coherentes.
Luego tengo algunos métodos de prueba que crean pares de objetos Thing
y los paso al método checkInvariants
parejas. Utilizo la partición de equivalencia para decidir qué pares valen la pena probar. Por lo general, creo cada par para que sea diferente en un solo atributo, más una prueba que prueba dos objetos equivalentes.
También a veces tengo un método de checkInvariants
3 argumentos, aunque encuentro que es menos útil para encontrar defectos, así que no lo hago a menudo
También puede usar algo similar a http://code.google.com/p/guava-libraries/source/browse/guava-testlib/src/com/google/common/testing/EqualsTester.java para probar equals y hashCode.
Vale la pena usar los complementos junit para esto. Consulte la clase EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ , puede ampliar esto e implementar createInstance y createNotEqualInstance, esto comprobará que los métodos equals y hashCode son correctos.
[En el momento de escribir estas líneas, se publicaron otras tres respuestas].
Para reiterar, el objetivo de mi pregunta es encontrar casos estándar de pruebas para confirmar que hashCode
e equals
está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:
- Escribió
equals()
sin escribirhashCode()
. Esto a menudo significa que la igualdad se definió como la igualdad de los campos de dos instancias. - Escribió
hashCode()
sin escribirequals()
. Esto puede significar que el programador buscaba un algoritmo de hash más eficiente.
En el caso del # 2, el problema parece inexistente para mí. 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 peor rendimiento para los mapas hash, lo cual está fuera del alcance de esta pregunta.
En el caso del n. ° 1, la prueba unitaria estándar implica crear dos instancias del mismo objeto con los mismos datos pasados al constructor, y verificar los mismos códigos hash. ¿Qué hay de los falsos positivos? Es posible elegir los parámetros del constructor que acaban de producir los mismos códigos hash en un algoritmo sin embargo poco sólido. Una prueba unitaria que tienda a evitar dichos parámetros cumpliría con el espíritu de esta pregunta. El atajo aquí es inspeccionar el código fuente para equals()
, pensar mucho 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 cumplir el espíritu de esta pregunta.
Por ejemplo, si la clase que se probará (llámela Datos) tiene un constructor que toma una Cadena, y las instancias construidas a partir de Cadenas que son equals()
arrojaron instancias que eran 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"))
, para forzar que el String no sea internado, aunque es más probable que arroje un código hash correcto que Data.equals()
es para producir un correcto resultado, en mi opinión.
La respuesta de Eli Courtwright es un ejemplo de una forma de romper el algoritmo de hash basado en el conocimiento de la especificación de equals
. El ejemplo de una colección especial es bueno, ya que las Collection
creadas por el usuario aparecen a veces, y son bastante propensas a bloqueos en el algoritmo hash.
EqualsVerifier es un proyecto de código abierto relativamente nuevo y hace un muy buen trabajo al probar el contrato igual. No tiene los issues EqualsTester de GSBase. Sin duda lo recomendaría.