Cómo probar las clases abstractas de la unidad: extender con talones?

Me preguntaba cómo probar las clases abstractas de la unidad y las clases que extienden las clases abstractas.

¿Debo probar la clase abstracta extendiéndola, anulando los métodos abstractos y luego probando todos los métodos concretos? Entonces solo prueba los métodos que anulo, y prueba los métodos abstractos en las pruebas unitarias para los objetos que extienden mi clase abstracta.

¿Debería tener un caso de prueba abstracto que pueda usarse para probar los métodos de la clase abstracta y extender esta clase en mi caso de prueba para los objetos que extienden la clase abstracta?

Tenga en cuenta que mi clase abstracta tiene algunos métodos concretos.

Escribe un objeto falso y úsalos solo para probar. Por lo general, son muy, muy mínimos (heredan de la clase abstracta) y no más. Luego, en su Prueba de unidad puede llamar al método abstracto que desea probar.

Deberías probar una clase abstracta que contenga alguna lógica como todas las otras clases que tienes.

Hay dos formas en que se utilizan clases base abstractas.

  1. Está especializando su objeto abstracto, pero todos los clientes usarán la clase derivada a través de su interfaz base.

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

Opción 1

Si tiene la primera situación, entonces realmente tiene una interfaz definida por los métodos virtuales en la clase abstracta que implementan 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.

IMotor

Esto significa que ahora puede probar su clase previamente abstracta 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 tienes la segunda situación, entonces tu clase abstracta funciona como una clase auxiliar.

AbstractHelper

Eche un vistazo a la funcionalidad que contiene. Vea si algo de eso se puede insertar en los objetos que se están manipulando para minimizar esta duplicación. Si aún le queda algo, mire cómo se convierte en una clase auxiliar que su implementación concreta tome en su constructor y elimine su clase base.

Motor Helper

Esto nuevamente conduce a clases concretas que son simples y fácilmente comprobables.


Como una regla

Favorecer una red compleja de objetos simples sobre una red simple de objetos complejos.

La clave del código extensible comprobable son los pequeños bloques de construcción y el cableado independiente.


Actualizado: ¿Cómo manejar las mezclas de ambos?

Es posible tener una clase base que realice estos dos roles … es decir: tiene una interfaz pública y tiene métodos de ayuda protegidos. Si este es el caso, entonces puede factorizar los métodos de ayuda 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, 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 alineadas correctamente, y necesita refactorización


Actualización 2: Clases abstractas como trampolín (2014/06/12)

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 dependency injection, una clase podría solicitar la configuración que le interesaba.

Implementé esto teniendo una clase base abstracta que sabe cómo analizar los formatos de archivos de configuración y las clases derivadas que expusieron esos mismos métodos, pero encapsulé la ubicación del archivo de configuración.

Podría haber escrito un “SettingsFileParser” que envolvió las 3 clases, y luego delegué en la clase base para exponer los métodos de acceso a los datos. Elegí no hacer esto todavía, ya que daría lugar 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. Cada configuración le pedirá a los usuarios algunas configuraciones y las transformará de alguna manera (dado que las configuraciones son texto, pueden envolverlas en objetos para convertirlas en números, etc.). A medida que esto ocurra, comenzaré a extraer esta lógica en métodos de manipulación de datos y los empujaré de regreso 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 será 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 ‘configuraciones’.

En ese momento, ya no querría que su interfaz pública incluyera los métodos de acceso a las configuraciones; entonces cambiaré esta clase para encapsular una clase de analizador de configuración en lugar de derivar de ella.

La clase Abstract es por lo tanto: 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. Puede que nunca lo consiga, por lo que puede vivir un buen rato … solo el código puede decirlo.

Encuentro 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. Te mantiene buscando la abstracción que te has perdido … y te permite continuar brindando valor a tu cliente mientras tanto.

Me imagino reglas como esta que definen un paisaje, donde el código sostenible vive en los valles. A medida que agrega un nuevo comportamiento, es como un aterrizaje forzoso en su código. Inicialmente lo pones donde sea que aterrice … luego se refactoriza para permitir que las fuerzas del buen diseño impulsen el comportamiento hasta que todo termine en los valles.

Lo que hago para las clases abstractas y las interfaces es lo siguiente: escribo una prueba, que usa el objeto como si fuera 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 subclases de ella, que tienen un método de configuración que establece la variable para una implementación concreta de X. De esta forma, 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.

Para realizar una prueba unitaria específicamente en la clase abstracta, debe derivarla para fines de prueba, probar los resultados de base.method () y el comportamiento previsto al heredar.

Prueba un método llamándolo para probar una clase abstracta implementándolo …

Si su clase abstracta contiene una funcionalidad concreta que tiene valor comercial, entonces usualmente la probaré directamente creando un doble de prueba que resuelva los datos abstractos, o utilizando un marco de burla 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 tengo que hacer esto es cuando estoy usando el patrón de 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 empresarial real ; no debe probar los detalles de implementación de la clase abstracta porque terminará con pruebas frágiles.

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). vea Probar una interfaz en el libro de recetas de junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

también vea la Superclase de Testcase en patrones xUnit: http://xunitpatterns.com/Testcase%20Superclass.html

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

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 tratando de 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.

Este es el patrón que suelo seguir cuando configuro un arnés para probar una clase abstracta:

public abstract class MyBase{ /*...*/ public abstract void VoidMethod(object param1); public abstract object MethodWithReturn(object param1); /*,,,*/ } 

Y la versión que uso bajo prueba:

 public class MyBaseHarness : MyBase{ /*...*/ public Action VoidMethodFunction; public override void VoidMethod(object param1){ VoidMethodFunction(param1); } public Func MethodWithReturnFunction; public override object MethodWithReturn(object param1){ return MethodWihtReturnFunction(param1); } /*,,,*/ } 

Si se llaman los métodos abstractos cuando no lo espero, las pruebas fallan. Al organizar las pruebas, puedo rescindir fácilmente los métodos abstractos con lambdas que realizan afirmaciones, lanzan excepciones, devuelven valores diferentes, etc.

Si los métodos concretos invocan cualquiera de los métodos abstractos, esa estrategia no funcionará, y querría probar cada comportamiento de clase secundario por separado. De lo contrario, extenderlo y anular los métodos abstractos tal como lo describió debería estar bien, de nuevo siempre que los métodos concretos de la clase abstracta estén desacoplados de las clases secundarias.

Supongo que podría querer probar la funcionalidad básica de una clase abstracta … Pero probablemente sea mejor si amplía la clase sin anular ningún método y hace una burla de mínimo esfuerzo para los métodos abstractos.

Una de las principales motivaciones para usar una clase abstracta es habilitar el polymorphism dentro de su aplicación, es decir, puede sustituir una versión diferente en el tiempo de ejecución. De hecho, esto es más o menos lo mismo que usar una interfaz, excepto que la clase abstracta proporciona una plomería común, a menudo llamada patrón de Plantilla .

Desde una perspectiva de prueba de unidad, hay dos cosas a considerar:

  1. 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 funciona bien con los demás.

  2. 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 impresionante marco de prueba falso que puede generar objetos falsos en tiempo de ejecución derivando dinámicamente de su clase. Este enfoque puede ahorrarle innumerables horas de clases derivadas de encoding manual.

Primero, si la clase abstracta contenía algún método concreto, creo que deberías hacer esto. Considera este ejemplo

  public abstract class A { public boolean method 1 { // concrete method which we have to test. } } class B extends class A { @override public boolean method 1 { // override same method as above. } } class Test_A { private static B b; // reference object of the class B @Before public void init() { b = new B (); } @Test public void Test_method 1 { b.method 1; // use some assertion statements. } } 

Siguiendo la respuesta de @ patrick-desjardins, implementé el resumen y su clase de implementación junto con @Test siguiente manera:

Clase Abstarct – ABC.java

 import java.util.ArrayList; import java.util.List; public abstract class ABC { abstract String sayHello(); public List getList() { final List defaultList = new ArrayList<>(); defaultList.add("abstract class"); return defaultList; } } 

Como las clases abstractas no se pueden crear instancias, pero se pueden subclasificar , la clase concreta DEF.java es la siguiente:

 public class DEF extends ABC { @Override public String sayHello() { return "Hello!"; } } 

@Test class para probar tanto el método abstracto como el no abstracto:

 import org.junit.Before; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.contains; import java.util.Collection; import java.util.List; import static org.hamcrest.Matchers.equalTo; import org.junit.Test; public class DEFTest { private DEF def; @Before public void setup() { def = new DEF(); } @Test public void add(){ String result = def.sayHello(); assertThat(result, is(equalTo("Hello!"))); } @Test public void getList(){ List result = def.getList(); assertThat((Collection) result, is(not(empty()))); assertThat(result, contains("abstract class")); } } 

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:

  • Se usa un simulacro en lugar de todas y cada una de las dependencias de la clase de sujeto que se prueba.
  • Un simulacro es una pseudo-implementación de una interfaz (puede recordar que, como regla general, las dependencias deben declararse como interfaces; la capacidad de prueba es una razón principal para esto)
  • Los comportamientos de los miembros de la interfaz del simulacro, ya sean métodos o propiedades, se suministran durante el tiempo de prueba (de nuevo, mediante el uso de un marco de burla). De esta forma, evita el acoplamiento de la implementación que se prueba con la implementación de sus dependencias (que deben tener todas sus propias pruebas discretas).