5

Quiero poder dividir una prueba grande en pruebas más pequeñas para que cuando pasen las pruebas más pequeñas implican que la prueba grande también pasará (por lo que no hay razón para ejecutar la prueba) gran prueba original). Quiero hacer esto porque las pruebas más pequeñas generalmente toman menos tiempo, menos esfuerzo y son menos frágiles. Me gustaría saber si hay patrones de diseño de prueba o herramientas de verificación que puedan ayudarme a lograr esta división de prueba de manera robusta.Dividir una prueba en un conjunto de pruebas más pequeñas

Me temo que la conexión entre las pruebas más pequeñas y la prueba original se pierde cuando alguien cambia algo en el conjunto de pruebas más pequeñas. Otro temor es que el conjunto de pruebas más pequeñas no cubra realmente la gran prueba.

Un ejemplo de lo que estoy apuntando en:

//Class under test 
class A { 

    public void setB(B b){ this.b = b; } 

    public Output process(Input i){ 
    return b.process(doMyProcessing(i)); 
    } 

    private InputFromA doMyProcessing(Input i){ .. } 

    .. 

} 

//Another class under test 
class B { 

    public Output process(InputFromA i){ .. } 

    .. 

} 

//The Big Test 
@Test 
public void theBigTest(){ 
A systemUnderTest = createSystemUnderTest(); // <-- expect that this is expensive 

Input i = createInput(); 

Output o = systemUnderTest.process(i); // <-- .. or expect that this is expensive 

assertEquals(o, expectedOutput()); 
} 

//The splitted tests 

@PartlyDefines("theBigTest") // <-- so something like this should come from the tool.. 
@Test 
public void smallerTest1(){ 
    // this method is a bit too long but its just an example.. 
    Input i = createInput(); 
    InputFromA x = expectedInputFromA(); // this should be the same in both tests and it should be ensured somehow 
    Output expected = expectedOutput(); // this should be the same in both tests and it should be ensured somehow 

    B b = mock(B.class); 
    when(b.process(x)).thenReturn(expected); 

    A classUnderTest = createInstanceOfClassA(); 
    classUnderTest.setB(b); 

    Output o = classUnderTest.process(i); 

    assertEquals(o, expected); 
    verify(b).process(x); 
    verifyNoMoreInteractions(b); 
} 

@PartlyDefines("theBigTest") // <-- so something like this should come from the tool.. 
@Test 
public void smallerTest2(){ 
    InputFromA x = expectedInputFromA(); // this should be the same in both tests and it should be ensured somehow 
    Output expected = expectedOutput(); // this should be the same in both tests and it should be ensured somehow 

    B classUnderTest = createInstanceOfClassB(); 

    Output o = classUnderTest.process(x); 

    assertEquals(o, expected); 
} 

Respuesta

2

La primera sugerencia que voy a hacer es volver a factorizar tus pruebas en rojo (falla). Para hacerlo, tendrás que romper tu código de producción temporalmente. De esta manera, sabes que las pruebas siguen siendo válidas.

Un patrón común es usar un accesorio de prueba por colección de pruebas "grandes". No tiene que seguir el patrón "todas las pruebas para una clase en una clase de prueba". Si un conjunto de pruebas se relacionan entre sí, pero no están relacionadas con otro conjunto de pruebas, colóquelas en su propia clase.

La mayor ventaja de utilizar una clase separada para realizar pequeñas pruebas individuales para la gran prueba es que puede aprovechar los métodos de instalación y desmontaje. En su caso, me gustaría mover las líneas que ha comentado con:

// this should be the same in both tests and it should be ensured somehow

con el método de configuración (en JUnit, un método anotado con @Before). Si tiene que realizar alguna configuración inusualmente costosa, la mayoría de los marcos de prueba xUnit tienen una forma de definir un método de configuración que se ejecuta una vez antes de todas las pruebas. En JUnit, este es un método public static void que tiene la anotación @BeforeClass.

Si los datos de la prueba son inmutables, tiendo a definir las variables como constantes.

Juntando todo esto, es posible que tenga algo como:

public class TheBigTest { 

    // If InputFromA is immutable, it could be declared as a constant 
    private InputFromA x; 
    // If Output is immutable, it could be declared as a constant 
    private Output expected; 

    // You could use 
    // @BeforeClass public static void setupExpectations() 
    // instead if it is very expensive to setup the data 
    @Before 
    public void setUpExpectations() throws Exception { 
     x = expectedInputFromA(); 
     expected = expectedOutput(); 
    } 

    @Test 
    public void smallerTest1(){ 
     // this method is a bit too long but its just an example.. 
     Input i = createInput(); 

     B b = mock(B.class); 
     when(b.process(x)).thenReturn(expected); 

     A classUnderTest = createInstanceOfClassA(); 
     classUnderTest.setB(b); 

     Output o = classUnderTest.process(i); 

     assertEquals(o, expected); 
     verify(b).process(x); 
     verifyNoMoreInteractions(b); 
    } 

    @Test 
    public void smallerTest2(){ 
     B classUnderTest = createInstanceOfClassB(); 

     Output o = classUnderTest.process(x); 

     assertEquals(o, expected); 
    } 

} 
+0

+1 Mantener las pruebas en una misma clase (y nombrar la clase de prueba como lo hizo) hará que sea menos probable que alguien pueda frenar accidentalmente la conexión entre la prueba original y las pruebas más pequeñas. Gracias por esto. Todavía me falta una forma automática de saber que las pruebas más pequeñas implican que la gran prueba pasaría. – mkorpela

+0

@mkorpela, ¿puede explicar un poco sobre por qué es importante que las pruebas más pequeñas impliquen que la gran prueba pasaría? Si falla una de las pruebas más pequeñas, ¿no es eso suficiente para indicar que existe un problema? En cualquier caso, la mayoría de los corredores de prueba tienen una indicación del estado de toda la clase de prueba. Por ejemplo, los corredores de prueba NUnit y Eclipse JUnit marcan la clase de prueba como "verde" si todas las pruebas en la clase pasan, y "roja" si fallan otras pruebas. "TheBigTest" se marcará como pasar si pasan todas las pruebas más pequeñas. –

+0

@Hurme, no puedo eliminar la prueba original si puede fallar en una situación en la que pasarían las pruebas más pequeñas. – mkorpela

0

Todo lo que puedo sugerir es el libro xUnit Test Patterns. Si hay una solución, debería estar allí.

0

theBigTest no se encuentra la dependencia de B. También smallerTest1 se burla de la dependencia B. En smallerTest2, debe simular InputFromA.

¿Por qué creó un gráfico de dependencia como lo hizo?

A toma un B luego cuando A::processInput, que luego puesto en proceso de InputFromAB.

Mantenga la prueba grande y refactorice A y B para cambiar la asignación de la dependencia.

[EDIT] en respuesta a las observaciones.

@mkorpela, mi punto es que al observar el código y sus dependencias es cómo comienza a tener una idea de cómo crear pruebas más pequeñas. A tiene una dependencia en B. Para completar su process(), debe usar B 's process(). Debido a esto, B tiene una dependencia en A.

+0

En primer lugar, es solo un ejemplo muy (demasiado) simple de lo que pretendo. Intento identificar las reglas/herramientas generales para dividir las pruebas de modo que el significado de la prueba original aún esté allí. theBigTest pierde la dependencia de B ya que la prueba está en un nivel más abstracto (no necesita saber que hay B pero el costo de no saber es que la prueba tardará más en ejecutarse). – mkorpela

+0

Entiendo ejemplos simples, pero mi punto es que 'B' es una referencia nula cuando ejecuta' systemUnderTest.process (i) '. No se ejecutará? Puede requerir que los dos extraigan el método o la clase en ese comportamiento. Puede requerir extraer el método o la clase para probar ese comportamiento. Además, si existe la posibilidad de que las dos pruebas pasen y existe la posibilidad de que falle la prueba grande, debe realizarse una tercera prueba que pruebe ese comportamiento defectuoso. – Gutzofter

+0

mi método createSystemUnderTest() inicializará systemUnderTest correctamente en el ejemplo inventado (suponiendo que B no será nulo). No es el mismo método utilizado en las otras pruebas. "Si existe la posibilidad de que las dos pruebas pasen y existe la posibilidad de que falle la prueba grande, debe realizarse una tercera prueba que pruebe ese comportamiento fallido". - Lo principal que estoy preguntando es cómo asegurar que las pruebas más pequeñas cubran la prueba original. – mkorpela

Cuestiones relacionadas