Me preguntaba cómo probar las clases abstractas de prueba y las clases que extienden las clases abstractas.
¿Debería probar la clase abstracta extendiéndola, eliminando los métodos abstractos y luego probar todos los métodos concretos? ¿Entonces solo pruebo los métodos que anulo y los métodos abstractos en las pruebas unitarias para objetos que extienden mi clase abstracta?
¿Debería tener un caso de prueba abstracta que pueda usarse para probar los métodos de la clase abstracta y extender esta clase en mi caso de prueba para objetos que extiendan la clase abstracta?
Tenga en cuenta que mi clase abstracta tiene algunos métodos concretos.
fuente
Hay dos formas en que se usan las clases base abstractas.
Está especializando su objeto abstracto, pero todos los clientes utilizarán la clase derivada a través de su interfaz base.
Está utilizando una clase base abstracta para factorizar la duplicación dentro de los objetos en su diseño, y los clientes usan las implementaciones concretas a través de sus propias interfaces.
Solución para 1 - Patrón de estrategia
Si tiene la primera situación, entonces tiene una interfaz definida por los métodos virtuales en la clase abstracta que están implementando sus clases derivadas.
Debería considerar hacer de esto una interfaz real, cambiar su clase abstracta para que sea concreta, y tomar una instancia de esta interfaz en su constructor. Sus clases derivadas se convierten en implementaciones de esta nueva interfaz.
Esto significa que ahora puede probar su clase abstracta anterior utilizando una instancia simulada de la nueva interfaz y cada nueva implementación a través de la interfaz ahora pública. Todo es simple y comprobable.
Solución para 2
Si tiene la segunda situación, su clase abstracta funciona como una clase auxiliar.
Eche un vistazo a la funcionalidad que contiene. Vea si algo de esto puede ser empujado sobre los objetos que están siendo manipulados para minimizar esta duplicación. Si aún le queda algo, considere convertirlo en una clase auxiliar que su implementación concreta tome en su constructor y elimine su clase base.
Esto nuevamente conduce a clases concretas que son simples y fácilmente comprobables.
Como una regla
Favorezca la red compleja de objetos simples sobre una red simple de objetos complejos.
La clave del código comprobable extensible son los pequeños bloques de construcción y el cableado independiente.
Actualizado: ¿Cómo manejar mezclas de ambos?
Es posible tener una clase base que realice ambos roles ... es decir, tiene una interfaz pública y tiene métodos auxiliares protegidos. Si este es el caso, puede factorizar los métodos auxiliares en una clase (escenario2) y convertir el árbol de herencia en un patrón de estrategia.
Si encuentra que tiene algunos métodos que su clase base implementa directamente y otros son virtuales, aún puede convertir el árbol de herencia en un patrón de estrategia, pero también lo tomaría como un buen indicador de que las responsabilidades no están correctamente alineadas, y puede necesita refactorización.
Actualización 2: Clases abstractas como un trampolín (12/06/2014)
El otro día tuve una situación en la que utilicé el resumen, así que me gustaría explorar por qué.
Tenemos un formato estándar para nuestros archivos de configuración. Esta herramienta en particular tiene 3 archivos de configuración, todos en ese formato. Quería una clase fuertemente tipada para cada archivo de configuración, por lo que, a través de la inyección de dependencia, una clase podría pedir la configuración que le interesaba.
Implementé esto al tener una clase base abstracta que sabe cómo analizar los formatos de los archivos de configuración y las clases derivadas que expusieron esos mismos métodos, pero encapsularon la ubicación del archivo de configuración.
Podría haber escrito un "SettingsFileParser" que envuelvan las 3 clases, y luego delegarlo en la clase base para exponer los métodos de acceso a datos. Elegí no hacer esto todavía, ya que conduciría a 3 clases derivadas con más código de delegación en ellas que cualquier otra cosa.
Sin embargo ... a medida que este código evoluciona y los consumidores de cada una de estas clases de configuración se vuelven más claros. Los usuarios de cada configuración solicitarán algunas configuraciones y las transformarán de alguna manera (como las configuraciones son texto, pueden envolverlas en objetos para convertirlas en números, etc.). A medida que esto suceda, comenzaré a extraer esta lógica en los métodos de manipulación de datos y los empujaré nuevamente a las clases de configuración fuertemente tipadas. Esto conducirá a una interfaz de nivel superior para cada conjunto de configuraciones, que eventualmente ya no es consciente de que se trata de 'configuraciones'.
En este punto, las clases de configuración fuertemente tipadas ya no necesitarán los métodos "getter" que exponen la implementación subyacente de 'configuración'.
En ese punto ya no querría que su interfaz pública incluyera los métodos de acceso a la configuración; así que cambiaré esta clase para encapsular una clase de analizador de configuraciones en lugar de derivarla.
Por lo tanto, la clase Resumen es: una forma de evitar el código de delegación en este momento y un marcador en el código para recordarme que cambie el diseño más adelante. Es posible que nunca llegue a él, por lo que puede vivir un buen rato ... solo el código puede decirlo.
Creo que esto es cierto con cualquier regla ... como "sin métodos estáticos" o "sin métodos privados". Indican un olor en el código ... y eso es bueno. Lo mantiene buscando la abstracción que se ha perdido ... y le permite seguir proporcionando valor a su cliente mientras tanto.
Me imagino reglas como esta que definen un paisaje, donde el código mantenible vive en los valles. A medida que agrega un nuevo comportamiento, es como que llueva en su código. Inicialmente lo pones donde aterriza ... luego refactorizas para permitir que las fuerzas del buen diseño impulsen el comportamiento hasta que todo termine en los valles.
fuente
Lo que hago para las clases abstractas y las interfaces es lo siguiente: escribo una prueba, que usa el objeto como es concreto. Pero la variable de tipo X (X es la clase abstracta) no se establece en la prueba. Esta clase de prueba no se agrega al conjunto de pruebas, sino a sus subclases, que tienen un método de configuración que establece la variable en una implementación concreta de X. De esa manera no duplico el código de prueba. Las subclases de la prueba no utilizada pueden agregar más métodos de prueba si es necesario.
fuente
Para realizar una prueba unitaria específicamente en la clase abstracta, debe derivarla para fines de prueba, resultados de prueba base.method () y comportamiento previsto al heredar.
Usted prueba un método llamándolo así que pruebe una clase abstracta al implementarlo ...
fuente
Si su clase abstracta contiene una funcionalidad concreta que tiene un valor comercial, generalmente la probaré directamente creando un doble de prueba que elimine los datos abstractos, o usando un marco de prueba para hacer esto por mí. El que elijo depende mucho de si necesito escribir implementaciones específicas de prueba de los métodos abstractos o no.
El escenario más común en el que necesito hacer esto es cuando estoy usando el patrón Método de plantilla , como cuando estoy construyendo algún tipo de marco extensible que será utilizado por un tercero. En este caso, la clase abstracta es lo que define el algoritmo que quiero probar, por lo que tiene más sentido probar la base abstracta que una implementación específica.
Sin embargo, creo que es importante que estas pruebas se centren únicamente en las implementaciones concretas de la lógica comercial real ; no debe probar los detalles de implementación de la clase abstracta porque terminará con pruebas frágiles.
fuente
una forma es escribir un caso de prueba abstracto que corresponda a su clase abstracta, luego escribir casos de prueba concretos que subclasifiquen su caso de prueba abstracta. haga esto para cada subclase concreta de su clase abstracta original (es decir, su jerarquía de casos de prueba refleja su jerarquía de clases). consulte Probar una interfaz en el libro de recetas junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .
también vea Testcase Superclass en xUnit patterns: http://xunitpatterns.com/Testcase%20Superclass.html
fuente
Yo argumentaría en contra de las pruebas "abstractas". Creo que una prueba es una idea concreta y no tiene una abstracción. Si tiene elementos comunes, colóquelos en métodos o clases auxiliares para que todos los usen.
En cuanto a probar una clase de prueba abstracta, asegúrese de preguntarse qué es lo que está probando. Hay varios enfoques, y debe averiguar qué funciona en su escenario. ¿Estás intentando probar un nuevo método en tu subclase? Luego haga que sus pruebas solo interactúen con ese método. ¿Estás probando los métodos en tu clase base? Luego, probablemente tenga un accesorio separado solo para esa clase, y pruebe cada método individualmente con tantas pruebas como sea necesario.
fuente
Este es el patrón que suelo seguir cuando configuro un arnés para probar una clase abstracta:
Y la versión que uso bajo prueba:
Si se invocan los métodos abstractos cuando no lo espero, las pruebas fallan. Al organizar las pruebas, puedo eliminar fácilmente los métodos abstractos con lambdas que realizan afirmaciones, lanzan excepciones, devuelven valores diferentes, etc.
fuente
Si los métodos concretos invocan cualquiera de los métodos abstractos, esa estrategia no funcionará, y usted querría probar el comportamiento de cada clase secundaria por separado. De lo contrario, extenderlo y tropezar con los métodos abstractos como ha descrito debería estar bien, una vez más, siempre que los métodos concretos de la clase abstracta estén desconectados de las clases secundarias.
fuente
Supongo que podría querer probar la funcionalidad básica de una clase abstracta ... Pero probablemente sería mejor extender la clase sin anular ningún método y hacer una burla de esfuerzo mínimo para los métodos abstractos.
fuente
Una de las principales motivaciones para usar una clase abstracta es habilitar el polimorfismo dentro de su aplicación, es decir: puede sustituir una versión diferente en tiempo de ejecución. De hecho, esto es casi lo mismo que usar una interfaz, excepto que la clase abstracta proporciona una plomería común, a menudo denominada patrón de plantilla .
Desde la perspectiva de las pruebas unitarias, hay dos cosas a considerar:
Interacción de su clase abstracta con las clases relacionadas . El uso de un marco de prueba simulado es ideal para este escenario, ya que muestra que su clase abstracta juega bien con los demás.
Funcionalidad de clases derivadas . Si tiene una lógica personalizada que ha escrito para sus clases derivadas, debe probar esas clases de forma aislada.
editar: RhinoMocks es un marco de prueba simulacro impresionante que puede generar objetos simulados en tiempo de ejecución derivando dinámicamente de su clase. Este enfoque puede ahorrarle innumerables horas de clases derivadas de codificación manual.
fuente
Primero, si la clase abstracta contiene algún método concreto, creo que deberías hacer esto considerando este ejemplo
fuente
Si una clase abstracta es apropiada para su implementación, pruebe (como se sugirió anteriormente) una clase concreta derivada. Tus suposiciones son correctas.
Para evitar confusiones futuras, tenga en cuenta que esta clase de prueba concreta no es una simulación, sino una falsificación .
En términos estrictos, un simulacro se define por las siguientes características:
fuente
Siguiendo la respuesta de @ patrick-desjardins, implementé abstract y su clase de implementación junto con
@Test
lo siguiente:Clase abstracta - ABC.java
Como las clases abstractas no se pueden instanciar, pero se pueden subclasificar , la clase concreta DEF.java es la siguiente:
@Test clase para probar tanto el método abstracto como el no abstracto:
fuente