¿Cómo probar las clases abstractas de prueba: extender con trozos?


446

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.

Respuestas:


268

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

Debe probar la clase abstracta que contiene cierta lógica como todas las demás clases que tiene.


99
Maldición, tengo que decir que esta es la primera vez que estoy de acuerdo con la idea de usar un simulacro.
Jonathan Allen

55
Necesitas dos clases, un simulacro y una prueba. La clase simulada solo extiende los métodos abstractos de la clase abstracta bajo prueba. Esos métodos pueden ser no-op, return null, etc. ya que no serán probados. La clase de prueba prueba solo la API pública no abstracta (es decir, la interfaz implementada por la clase abstracta). Para cualquier clase que amplíe la clase Resumen, necesitará clases de prueba adicionales porque los métodos abstractos no estaban cubiertos.
cyber-monk

10
Obviamente, es posible hacer esto ... pero para probar verdaderamente cualquier clase derivada, va a probar esta funcionalidad básica una y otra vez ... lo que lo lleva a tener un dispositivo de prueba abstracto para que pueda deducir esta duplicación en la prueba. ¡Todo esto huele! En primer lugar, recomiendo echar un vistazo a por qué está usando clases abstractas y ver si algo más funcionaría mejor.
Nigel Thorne

55
La siguiente respuesta sobrevalorada es mucho mejor.
Martin Spamer

22
@MartiSpamer: No diría que esta respuesta está sobrevalorada porque fue escrita mucho antes (2 años) que la respuesta que consideras mejor a continuación. Alentemos a Patrick porque en el contexto de cuando publicó esta respuesta, fue genial. Animémonos unos a otros. Saludos
Marvin Thobejane

449

Hay dos formas en que se usan las clases base abstractas.

  1. Está especializando su objeto abstracto, pero todos los clientes utilizará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 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.

IMotor

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.

AbstractHelper

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.

Motor Helper

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.


18
Esta es una respuesta genial. Mucho mejor que el mejor clasificado. Pero supongo que solo aquellos que realmente quieran escribir código comprobable lo apreciarían ... :)
MalcomTucker

22
No puedo superar lo buena que es esta respuesta. Ha cambiado totalmente mi forma de pensar sobre las clases abstractas. Gracias Nigel
MalcomTucker

44
¡Oh, no ... otro principio que voy a tener que repensar! Gracias (tanto sarcásticamente por ahora, como no sarcásticamente por una vez lo he asimilado y me siento como un mejor programador)
Martin Lyne

11
Buena respuesta. Definitivamente algo en lo que pensar ... ¿pero no se reduce básicamente lo que estás diciendo a no usar clases abstractas?
Brianestey

32
+1 solo para la regla, "Favorezca la red compleja de objetos simples sobre una red simple de objetos complejos".
David Glass

12

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.


¿No causa esto problemas de conversión en la subclase? si X tiene el método a e Y hereda X pero también tiene un método b. Cuando subclases tu clase de prueba, ¿no tienes que convertir tu variable abstracta en una Y para realizar pruebas en b?
Johnno Nolan

8

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


8

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.


6

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


4

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.


No quería volver a probar el código que ya había probado, por eso iba por el camino del caso de prueba abstracta. Estoy tratando de probar todos los métodos concretos en mi clase abstracta en un solo lugar.
Paul Whelan

77
No estoy de acuerdo con extraer elementos comunes a clases auxiliares, al menos en algunos (¿muchos?) Casos. Si una clase abstracta contiene alguna funcionalidad concreta, creo que es perfectamente aceptable probar la funcionalidad de esa unidad directamente.
Seth Petry-Johnson

4

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<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

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.


3

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.


2

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.


2

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:

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


2

Primero, si la clase abstracta contiene algún método concreto, creo que deberías hacer esto considerando 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.

       }

   }

1

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 utiliza un simulacro en lugar de todas y cada una de las dependencias de la clase de sujeto que se está probando.
  • 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 de las razones principales para esto)
  • Los comportamientos de los miembros de la interfaz del simulacro, ya sean métodos o propiedades, se proporcionan en el momento de la prueba (de nuevo, mediante el uso de un marco de simulación). De esta forma, evita el acoplamiento de la implementación que se está probando con la implementación de sus dependencias (que deberían tener sus propias pruebas discretas).

1

Siguiendo la respuesta de @ patrick-desjardins, implementé abstract y su clase de implementación junto con @Testlo siguiente:

Clase abstracta - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

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

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

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

@Test clase 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<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.