java - intellij - junit test android
¿Cómo probar la igualdad de gráficos de objetos complejos? (9)
Digamos que tengo una prueba de unidad que quiere comparar dos complejos por objetos para la igualdad. Los objetos contienen muchos otros objetos profundamente anidados. Todas las clases de los objetos han definido correctamente los métodos equals()
.
Esto no es dificil
@Test
public void objectEquality() {
Object o1 = ...
Object o2 = ...
assertEquals(o1, o2);
}
El problema es que, si los objetos no son iguales, todo lo que obtiene es un error, sin ninguna indicación de qué parte del gráfico de objetos no coincide. Depurar esto puede ser doloroso y frustrante.
Mi enfoque actual es asegurarse de que todo implementa toString()
, y luego comparar la igualdad como esta:
assertEquals(o1.toString(), o2.toString());
Esto hace que sea más fácil encontrar errores en las pruebas, ya que los IDE como Eclipse tienen un comparador visual especial para mostrar las diferencias de cadena en las pruebas fallidas. Esencialmente, los gráficos de objetos se representan textualmente, para que pueda ver dónde está la diferencia. Siempre que toString()
esté bien escrito, funciona muy bien.
Sin embargo, todo es un poco torpe. A veces, desea diseñar toString () para otros fines, como el registro, tal vez solo quiera representar algunos de los campos de objetos en lugar de todos, o tal vez toString () no esté definido en absoluto, y así sucesivamente.
Estoy buscando ideas para una mejor manera de comparar gráficos de objetos complejos. ¿Alguna idea?
Debido a la forma en que tiendo a diseñar objetos complejos, aquí tengo una solución muy fácil.
Al diseñar un objeto complejo para el que necesito escribir un método igual (y, por lo tanto, un método hashCode), tiendo a escribir un renderizador de cadenas y usar los métodos equing y hashCode de la clase String.
El renderizador, por supuesto, no es una cadena: realmente no tiene que ser fácil de leer para los humanos, e incluye todos y solo los valores que necesito comparar, y por costumbre los pongo en el orden que controla la forma en que lo hago. querría que lo ordenaran; Ninguno de los cuales es necesariamente cierto del método toString.
Naturalmente, almaceno en caché esta cadena representada (y también el valor de hashCode). Normalmente es privado, pero dejar el paquete de cadenas en caché privado le permitiría verlo desde sus pruebas unitarias.
Por cierto, esto no siempre es lo que termino con los sistemas entregados, por supuesto, si las pruebas de rendimiento muestran que este método es demasiado lento, estoy preparado para reemplazarlo, pero ese es un caso raro. Hasta ahora, solo sucedió una vez, en un sistema en el que los objetos mutables se cambiaban rápidamente y se comparaban con frecuencia.
La razón por la que hago esto es que escribir un buen código hash no es trivial , y requiere una prueba (*), mientras que hacer uso de uno en String evita la prueba.
(* Considere que el paso 3 en la receta de Josh Bloch para escribir un buen método de hashCode es probarlo para asegurarse de que los objetos "iguales" tengan los mismos valores de hashCode, y asegurarse de haber cubierto todas las posibles variaciones que están cubiertas no es trivial en sí mismo. Más sutil e incluso más difícil de probar bien es la distribución)
El Blog de desarrolladores de Atlassian tenía algunos artículos sobre este mismo tema, y cómo la biblioteca de Hamcrest puede hacer que la depuración de este tipo de prueba sea muy simple:
- Cómo Hamcrest puede salvar tu alma (parte 1)
- Hamcrest salva tu alma - ¡Ahora con menos sufrimiento! (parte 2)
Básicamente, para una afirmación como esta:
assertThat(lukesFirstLightsaber, is(equalTo(maceWindusLightsaber)));
Hamcrest le devolverá la salida como esta (en la que solo se muestran los campos que son diferentes):
Expected: is {singleBladed is true, color is PURPLE, hilt is {...}}
but: is {color is GREEN}
El código para este problema existe en http://code.google.com/p/deep-equals/
Utilice DeepEquals.deepEquals (a, b) para comparar dos objetos Java para la igualdad semántica. Esto comparará los objetos utilizando cualquier método personalizado equals () que puedan tener (si tienen un método equals () implementado que no sea Object.equals ()). Si no, este método procederá entonces a comparar los objetos campo por campo, recursivamente. A medida que se encuentra cada campo, intentará usar los equivalentes derivados () si existe, de lo contrario continuará repitiendo.
Este método funcionará en un gráfico de objetos cíclicos como este: A-> B-> C-> A. Tiene detección de ciclo por lo que CUALQUIER dos objetos pueden compararse, y nunca entrará en un bucle sin fin.
Use DeepEquals.hashCode (obj) para calcular un hashCode () para cualquier objeto. Al igual que deepEquals (), intentará llamar al método hashCode () si se implementa un método personalizado hashCode () (debajo de Object.hashCode ()), de lo contrario, computará el campo hashCode campo por campo, recursivamente (Profundo). También como deepEquals (), este método manejará gráficos de objetos con ciclos. Por ejemplo, A-> B-> C-> A. En este caso, hashCode (A) == hashCode (B) == hashCode (C). DeepEquals.deepHashCode () tiene detección de ciclos y, por lo tanto, funcionará en CUALQUIER gráfico de objetos.
Las pruebas unitarias deben tener bien definidas, lo único que prueban. Esto significa que al final deberías tener algo bien definido y único que puede ser diferente acerca de esos dos objetos. Si hay demasiadas cosas que pueden diferir, sugeriría dividir esta prueba en varias pruebas más pequeñas.
Lo que podría hacer es renderizar cada objeto a XML usando XStream y luego usar XMLUnit para realizar una comparación en el XML. Si difieren, entonces obtendrá la información contextual (en forma de un XPath, IIRC) que le indicará dónde difieren los objetos.
por ejemplo, desde el documento XMLUnit:
Comparing test xml to control xml [different]
Expected element tag name ''uuid'' but was ''localId'' -
comparing <uuid...> at /msg[1]/uuid[1] to <localId...> at /msg[1]/localId[1]
Tenga en cuenta la XPath que indica la ubicación de los diferentes elementos.
Probablemente no sea rápido, pero eso puede no ser un problema para las pruebas unitarias.
No usaría toString()
porque, como usted dice, generalmente es más útil para crear una representación agradable del objeto para fines de visualización o registro.
Me parece que su prueba de "unidad" no está aislando la unidad bajo prueba. Si, por ejemplo, su gráfico de objetos es A-->B-->C
y está probando A
, a su prueba unitaria para A
no le debe importar que el método equals()
en C
esté funcionando. Su prueba de unidad para C
se aseguraría de que funcione.
Así que probaría lo siguiente en la prueba para el método equals()
: - compare dos objetos A que tienen B
idénticas, en ambas direcciones, por ejemplo a1.equals(a2)
y a2.equals(a1)
. - comparar dos objetos A
que tienen diferentes B
, en ambas direcciones
Al hacerlo de esta manera, con una afirmación JUnit para cada comparación, sabrá dónde está la falla.
Obviamente, si su clase tiene más hijos que son parte de la determinación de la igualdad, necesitaría probar muchas más combinaciones. Sin embargo, lo que estoy tratando de entender es que su examen de unidad no debería preocuparse por el comportamiento de nada más allá de las clases con las que tiene contacto directo. En mi ejemplo, eso significa que asumirías que C.equals()
funciona correctamente.
Una arruga puede ser si está comparando colecciones. En ese caso, usaría una utilidad para comparar colecciones, como commons-collections CollectionUtils.isEqualCollection()
. Por supuesto, solo para colecciones en su unidad bajo prueba.
Seguí la misma pista en la que estás. También tuve problemas adicionales:
- no podemos modificar las clases (para iguales o toString) que no tenemos (JDK), matrices, etc.
- La igualdad es a veces diferente en varios contextos.
Por ejemplo, el seguimiento de la igualdad de las entidades puede depender de los identificadores de la base de datos cuando estén disponibles (concepto de "misma fila"), confíe en la igualdad de algunos campos (la clave de negocio) (para los objetos no guardados). Para la afirmación de Junit, es posible que desee que todos los campos sean iguales.
Así que terminé creando objetos que se ejecutan a través de un gráfico, haciendo su trabajo sobre la marcha.
Normalmente hay un objeto de rastreo de superclase:
rastrear a través de todas las propiedades de los objetos; detente en:
- enums
- clases marco (si corresponde),
- en proxies descargados o conexiones distantes,
- en objetos ya visitados (para evitar bucles)
- en la relación de muchos a uno, si indican un padre (generalmente no incluido en el semántico igual)
- ...
configurable para que pueda detenerse en algún momento (detenerse completamente o detener el rastreo dentro de la propiedad actual):
- cuando los métodos mustStopCurrent () o mustStopCompletely () devuelven verdadero,
- al encontrar algunas anotaciones en un captador o una clase,
- cuando el actual (class, getter) pertenece a una lista de excepciones
- ...
A partir de esa superclase de rastreo, las subclases están hechas para muchas necesidades:
- Para crear una cadena de depuración (llamar a toString según sea necesario, con casos especiales para colecciones y matrices que no tienen un buen toString; manejo de un límite de tamaño, y mucho más).
- Para crear varios ecualizadores (como se dijo antes, para Entidades que usan identificadores, para todos los campos, o únicamente basados en iguales;). Estos ecualizadores a menudo también necesitan casos especiales (por ejemplo, para clases fuera de su control).
Volviendo a la pregunta: estos ecualizadores podrían recordar el camino a los diferentes valores , que sería muy útil para su caso JUnit para comprender la diferencia.
- Para la creación de Ordenadores . Por ejemplo, las entidades de guardado deben realizarse es un orden específico, y la eficiencia dictará que guardar las mismas clases juntas dará un gran impulso.
- Para recopilar un conjunto de objetos que se pueden encontrar en varios niveles en el gráfico. Hacer bucle en el resultado del colector es muy fácil.
Como complemento, debo decir que, a excepción de las entidades en las que el rendimiento es una preocupación real, elegí esa tecnología para implementar toString (), hashCode (), equals () y compareTo () en mis entidades.
Por ejemplo, si una clave de negocio en uno o más campos se define en Hibernate a través de @UniqueConstraint en la clase, supongamos que todas mis entidades tienen una propiedad getIdent () implementada en una superclase común. La superclase de mi entidad tiene una implementación predeterminada de estos 4 métodos que se basan en este conocimiento, por ejemplo (es necesario cuidar los nulos):
- toString () imprime "myClass (key1 = value1, key2 = value2)"
- hashCode () es "value1.hashCode () ^ value2.hashCode ()"
- equals () es "value1.equals (other.value1) && value2.equals (other.value2)"
- compareTo () es combinar la comparación de la clase, valor1 y valor2.
Para las entidades en las que el rendimiento es importante, simplemente anulo estos métodos para no usar la reflexión. Puedo probar en regresión JUnit prueba que las dos implementaciones se comportan de manera idéntica.
Si estás dispuesto a que tus pruebas estén escritas en matchete puedes usar un matchete . Es una colección de emparejadores que se pueden usar con JUnit y proporcionan, entre otras cosas, la capacidad de comparar gráficos de objetos :
case class Person(name: String, age: Int, address: Address)
case class Address(street: String)
Person("john",12, Address("rue de la paix")) must_== Person("john",12,Address("rue du bourg"))
Producirá el siguiente mensaje de error
org.junit.ComparisonFailure: Person(john,12,Address(street)) is not equal to Person(john,12,Address(different street))
Got : address.street = ''rue de la paix''
Expected : address.street = ''rue du bourg''
Como puede ver aquí, he estado usando clases de casos, que son reconocidas por matchete para sumergirse en el gráfico de objetos. Esto se hace a través de una clase de tipo llamada Diffable
. No voy a discutir las clases de tipos aquí, así que digamos que es la piedra angular de este mecanismo, que compara 2 instancias de un tipo dado. Los tipos que no son clases de casos (así que básicamente todos los tipos en Java) obtienen un Diffable
predeterminado que usa equals
. Esto no es muy útil, a menos que proporcione un Diffable
para su tipo particular:
// your java object
public class Person {
public String name;
public Address address;
}
// you scala test code
implicit val personDiffable : Diffable[Person] = Diffable.forFields(_.name,_.address)
// there you go you can now compare two person exactly the way you did it
// with the case classes
Así que hemos visto que el matchete funciona bien con una base de código Java. De hecho, he estado usando matchete en mi último trabajo en un gran proyecto de Java.
Descargo de responsabilidad: soy el autor matchete :)
Usamos una biblioteca llamada junitx para probar el contrato de igualdad en todos nuestros objetos "comunes": http://www.extreme-java.de/junitx/
La única forma en que puedo pensar para probar las diferentes partes de su método equals () es dividir la información en algo más granular. Si está probando un árbol de objetos profundamente anidado, lo que está haciendo no es realmente una prueba de unidad. Debe probar el contrato equals () en cada objeto individual en el gráfico con un caso de prueba separado para ese tipo de objeto. Puede usar objetos de código auxiliar con una implementación simplista de equals () para los campos de tipo de clase en el objeto bajo prueba.
HTH