2010-05-11 16 views
80

Durante los últimos años, siempre pensé que en Java, Reflection se utiliza ampliamente 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.¿Es una mala práctica usar Reflection in Unit testing?

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 que-

Edit: Debo haber mencionado que me requirieron que todas las pruebas están en un paquete separado llamado prueba (así que usar visibilidad protegida por ejemplo, no era una solución posible también)

+0

¿Qué tipo de acceso practicas con reflexión, conjunto, obtención o ambos? – fish

+0

@fish: uso solo para verificar si se configuran algunos valores específicos – RoflcoptrException

+7

Es una práctica normal en Java colocar las pruebas en el mismo paquete para que pueda probar las clases de "paquete privado" (acceso predeterminado). –

Respuesta

58

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 las pruebas de su unidad, generalmente significa que la clase en cuestión tiene una interfaz inadecuada y/o trata de 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 entender 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 @tackline señaló, esto solo se refiere a la utilización de Reflection dentro del código de prueba propio, no de las partes internas del framework de pruebas. 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.

+9

La reflexión es obviamente útil dentro del marco de prueba para las pruebas de llamada y las interfaces de burla, como una alternativa a los procesadores de anotaciones en tiempo de compilación. Pero debe notarse claramente como un tipo de uso separado. –

+1

@tackline, esto es lo que realmente quise decir, gracias por señalarlo. Agregué una aclaración a mi respuesta. –

+0

No necesita muchos calificadores. La reflexión debería usarse principalmente como último recurso, punto. Hace que el razonamiento sobre el programa sea efectivamente imposible. –

56

Es realmente malo para modificar la visibilidad de una API de producción solo por el bien de la prueba. 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 la prueba unitaria es mayoritariamente correcta. Por supuesto, debe design your classes for testability, por lo que se requiere 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 "qué & no lo hacen", y se refiere a lo que debería realizarse una prueba - si el estado interno de los objetos tiene que ser probado o no; si debemos permitirnos cuestionar el diseño de la clase bajo prueba; etc.

+1

¿Podría explicar su voto a favor? – Bozho

+6

Hay dos lados en el cambio de API de producción. Cambiarlo solo para las pruebas es malo, mientras que actualizar su API/diseño para que su código sea más comprobable (y por lo tanto no está cambiando su visibilidad solo para las pruebas) es una buena práctica. – deterb

+1

si está seguro de que lo que está haciendo no tiene efectos secundarios, sí. ;) – Bozho

7

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, tiene 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 deficiente usar 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.

EDIT: en respuesta a su edición, que es, al menos, una práctica tan pobre como usar Reflection, 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, todavía se puede solucionar este problema mediante el uso protegido (por desgracia no paquete-privada, que es realmente ideal para esto) ensayando una subclase de esta manera:

public class ClassUnderTest { 
     protect void methodExposedForTesting() {} 
} 

Y dentro de su unidad de prueba

class ClassUnderTestTestable extends ClassUnderTest { 
    @Override public void methodExposedForTesting() { super.methodExposedForTesting() } 
} 

Y si tiene un constructor protected:

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".

+0

+1 Para cambiar la visibilidad del código de producción no es una buena solución –

3

Creo que su 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.

28

Desde la perspectiva de TDD - Test Driven Diseño - 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, usamos 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.

+3

De acuerdo. Trate el objeto que se prueba como una caja negra: asegúrese de que las salidas coincidan con las entradas. Si no saben, tienen un problema con las partes internas de la implementación. – AngerClown

+0

Pero, ¿qué pasa con probar el estado del objeto? –

+5

El estado interno del objeto no importa, siempre que su comportamiento externo sea correcto. –

3

que añadir a lo que han dicho otros, 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 SomeApplicationClass.computeSquare privada(), pasando de un número entero y devolver un resultado de cadena.Esto resulta en una prueba JUnit que compilará bien, pero fallar durante ejecución si cualquiera de los siguientes casos:

  • Nombre de método "computeSquare" se cambia el nombre
  • El método toma en un tipo de parámetro diferente (por ejemplo, cambiar de entero a Long)
  • El número de cambio de parámetros (por ejemplo, pasando en otro parámetro)
  • El tipo de retorno de los cambios de método (quizás de cadena a entero)

en cambio, si hay No es una manera fácil de demostrar que el cálculo de Square 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. Tire de este método en una nueva clase que le da la siguiente prueba:

public void testSquare() 
{ 
    String result = new NumberUtils().computeSquare(new Integer(3)); 
    assertEquals("9", result); 
} 

ahora (sobre todo cuando se utilizan las herramientas de refactorización disponibles en la moderna IDE), cambiando el nombre del método no tiene ningún efecto sobre la prueba (como se su IDE también habrá refactorizado la prueba JUnit también), mientras que al cambiar el tipo de parámetro, el número de parámetros o el tipo de retorno marcará un error de compilación en su prueba de Junit, lo que significa que no va a verificar una prueba JUnit que compila pero falla en tiempo de ejecución.

Mi punto final es que a veces, especialmente cuando se trabaja con código heredado, y necesita agregar nuevas funcionalidades, puede no ser 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.

5

Secundo la idea de Bozho:

Es muy malo para modificar la visibilidad de una API de producción sólo por el bien de la prueba.

Pero si intenta do the simplest thing that could possibly work, entonces puede ser preferible escribir el texto 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.

Después de haber caído en la trampa de la accesibilidad relajante sólo para las pruebas y luego acceder a inadvertidamente el método fue privado de otro código de producción pensé en dp4j.jar: se inyecta el código API de reflexión (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 example of dp4j with JUnit.