java - ¿Es una mala práctica usar Reflection in Unit testing?
unit-testing (7)
Creo que tu código debe probarse de dos maneras. Debes probar tus métodos públicos a través de Unit Test y eso actuará como nuestra prueba de caja negra. Dado que su código se divide en funciones manejables (buen diseño), querrá probar las piezas individuales con reflexión para asegurarse de que funcionen independientemente del proceso, la única forma en que puedo pensar es en reflexionar desde ellos son privados
Al menos, este es mi pensamiento en el proceso de prueba unitaria.
Esta pregunta ya tiene una respuesta aquí:
Durante los últimos años siempre pensé que en Java, la Reflexión es ampliamente utilizada durante las pruebas unitarias. Dado que algunas de las variables / métodos que deben verificarse son privados, de alguna manera es necesario leer los valores de los mismos. Siempre pensé que la Reflection API también se usa para este propósito.
La semana pasada tuve que probar algunos paquetes y, por lo tanto, escribir algunas pruebas JUnit. Como siempre, utilicé Reflection para acceder a campos y métodos privados. Pero mi supervisor que verificó el código no estaba muy contento con eso y me dijo que la Reflection API no estaba destinada a usarse para tal "piratería". En cambio, sugirió modificar la visibilidad en el código de producción.
¿Es realmente una mala práctica usar Reflection? Realmente no puedo creer eso-
Editar: Debería haber mencionado que se me exigió que todas las pruebas estén en un paquete separado llamado prueba (por lo que usar visibilidad protegida, por ejemplo, tampoco era una solución posible)
Desde la perspectiva de TDD - Test Driven Design - esta es una mala práctica. Sé que no etiquetaste este TDD, ni preguntaste específicamente sobre él, pero TDD es una buena práctica y esto va en contra de su grano.
En TDD, utilizamos nuestras pruebas para definir la interfaz de la clase, la interfaz pública , y por lo tanto estamos escribiendo pruebas que interactúan directamente solo con la interfaz pública. Nos preocupa esa interfaz; los niveles de acceso apropiados son una parte importante del diseño, una parte importante del buen código. Si te encuentras en la necesidad de probar algo privado, por lo general, según mi experiencia, es un olor de diseño.
En mi opinión, la reflexión en mi humilde opinión solo debería ser un último recurso, reservado para el caso especial del código heredado de pruebas unitarias o una API que no puede cambiar. Si está probando su propio código, el hecho de que necesite utilizar Reflection significa que su diseño no es comprobable, por lo que debe solucionarlo en lugar de recurrir a Reflection.
Si necesita acceder a miembros privados en sus pruebas de unidad, generalmente significa que la clase en cuestión tiene una interfaz inadecuada y / o intenta hacer demasiado. Por lo tanto, su interfaz debe ser revisada, o algún código debe ser extraído en una clase separada, donde esos métodos problemáticos / acceso de campo pueden hacerse públicos.
Tenga en cuenta que el uso de Reflection en general da como resultado un código que, además de ser más difícil de comprender y mantener, también es más frágil. Hay un conjunto completo de errores que, en el caso normal, serían detectados por el compilador, pero con Reflection solo aparecen como excepciones de tiempo de ejecución.
Actualización: como se señaló en @tackline, esto solo concierne al uso de Reflection dentro del propio código de prueba, no de las partes internas del framework de prueba. JUnit (y probablemente todos los demás marcos similares) utiliza la reflexión para identificar y llamar a sus métodos de prueba: este es un uso justificado y localizado de la reflexión. Sería difícil o imposible proporcionar las mismas características y conveniencia sin usar Reflection. OTOH está completamente encapsulado dentro de la implementación del marco, por lo que no complica ni compromete nuestro propio código de prueba.
En segundo lugar, el pensamiento de Bozho:
Es realmente malo modificar la visibilidad de una API de producción simplemente por el bien de las pruebas.
Pero si intenta hacer lo más simple que pueda funcionar, entonces puede ser preferible escribir la plantilla repetitiva de Reflection API, al menos mientras su código sea nuevo / cambiante. Quiero decir, la carga de cambiar manualmente las llamadas reflejadas de su prueba cada vez que cambie el nombre del método, o params, es en mi humilde opinión demasiado carga y el enfoque equivocado.
Habiendo caído en la trampa de relajar la accesibilidad solo para probar y luego acceder inadvertidamente al método was-private de otro código de producción, pensé en dp4j.jar : inyecta el código Reflection API ( Lombok-style ) para que no cambie el código de producción Y no escriba la API Reflection usted mismo; dp4j reemplaza su acceso directo en la prueba de unidad con la API de reflexión equivalente en tiempo de compilación. Aquí hay un ejemplo de dp4j con JUnit .
Es realmente malo modificar la visibilidad de una API de producción simplemente por el bien de las pruebas. Es probable que esa visibilidad se establezca en su valor actual por razones válidas y no se modifique.
El uso de la reflexión para las pruebas unitarias es en general bueno. Por supuesto, debe diseñar sus clases para probarlas , de modo que se requiera menos reflexión.
Spring, por ejemplo, tiene ReflectionTestUtils
. Pero su propósito es establecer burlas de dependencias, donde se suponía que la primavera las inyectaba.
El tema es más profundo que "do y do not", y se refiere a lo que se debe probar: si el estado interno de los objetos debe ser probado o no; si debemos permitirnos cuestionar el diseño de la clase bajo prueba; etc.
Lo consideraría una mala práctica, pero simplemente cambiar la visibilidad en el código de producción no es una buena solución, tienes que ver la causa. O bien tiene una API no comprobable (es decir, la API no se expone lo suficiente como para que una prueba se controle), por lo que está buscando probar el estado privado, o sus pruebas están demasiado unidas a su implementación, lo que las hará solo uso marginal cuando refactorizas.
Sin saber más acerca de su caso, realmente no puedo decir más, pero de hecho se considera una práctica pobre usar la reflection . Personalmente, preferiría convertir la prueba en una clase interna estática de la clase sometida a prueba que recurrir a la reflexión (si dijera que la parte no comprobable de la API no estaba bajo mi control), pero algunos lugares tendrán un problema mayor con el código de prueba en el mismo paquete que el código de producción que con el uso de la reflexión.
EDITAR: en respuesta a su edición, esa es al menos una práctica tan pobre como usar Reflexión, probablemente peor. La forma en que normalmente se maneja es usar el mismo paquete, pero mantener las pruebas en una estructura de directorio separada. Si las pruebas unitarias no pertenecen al mismo paquete que la clase bajo prueba, no sé lo que hace.
De todos modos, aún puedes evitar este problema usando protected (lamentablemente no package-private, que es realmente ideal para esto) probando una subclase como esta:
public class ClassUnderTest {
protect void methodExposedForTesting() {}
}
Y dentro de tu unidad de prueba
class ClassUnderTestTestable extends ClassUnderTest {
@Override public void methodExposedForTesting() { super.methodExposedForTesting() }
}
Y si tienes un constructor protegido:
ClassUnderTest test = new ClassUnderTest(){};
No necesariamente recomiendo lo anterior para situaciones normales, pero sí las restricciones a las que se le pide que trabaje y que ya no sean las "mejores prácticas".
Para agregar a lo que otros han dicho, considere lo siguiente:
//in your JUnit code...
public void testSquare()
{
Class classToTest = SomeApplicationClass.class;
Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class});
String result = (String)privateMethod.invoke(new Integer(3));
assertEquals("9", result);
}
Aquí, estamos usando la reflexión para ejecutar el método privado SomeApplicationClass.computeSquare (), pasando un entero y retornando un resultado de cadena. Esto da como resultado una prueba JUnit que compilará bien pero fallará durante la ejecución si se produce cualquiera de las siguientes situaciones:
- El nombre del método "computeSquare" se renombra
- El método toma un tipo de parámetro diferente (por ejemplo, cambiar entero a largo)
- El número de parámetros cambia (por ejemplo, pasar otro parámetro)
- El tipo de devolución del método cambia (quizás de String a Integer)
En cambio, si no hay una manera fácil de probar que el computeSquare ha hecho lo que se supone que debe hacer a través de la API pública, entonces su clase probablemente esté tratando de hacer mucho. Lleve este método a una nueva clase que le proporcione la siguiente prueba:
public void testSquare()
{
String result = new NumberUtils().computeSquare(new Integer(3));
assertEquals("9", result);
}
Ahora (especialmente cuando usa las herramientas de refactorización disponibles en IDE modernos), cambiar el nombre del método no tiene ningún efecto en su prueba (ya que su IDE también habrá refactorizado la prueba JUnit también), mientras cambia el tipo de parámetro, el número de los parámetros o el tipo de retorno del método marcarán un error de compilación en la prueba de Junit, lo que significa que no se comprobará una prueba JUnit que se compila pero falla en el tiempo de ejecución.
Mi punto final es que a veces, especialmente cuando se trabaja con código heredado, y se necesita agregar nuevas funcionalidades, puede que no sea fácil hacerlo en una clase separada bien escrita y comprobable. En este caso, mi recomendación sería aislar los nuevos cambios de código en los métodos de visibilidad protegidos que puede ejecutar directamente en su código de prueba JUnit. Esto le permite comenzar a construir una base de código de prueba. Eventualmente, debe refactorizar la clase y extraer su funcionalidad agregada, pero, mientras tanto, la visibilidad protegida en su nuevo código a veces puede ser su mejor opción en términos de capacidad de prueba sin una refactorización importante.