¿Cómo puedo diseñar casos de prueba para cubrir código basado en eventos aleatorios?

15

Por ejemplo, si el código genera un int aleatorio de 0-10, y toma una rama diferente en cada resultado, ¿cómo se puede diseñar un conjunto de pruebas para garantizar una cobertura del estado del 100% en dicho código?

En Java, el código podría ser algo como:

int i = new Random().nextInt(10);
switch(i)
{
    //11 case statements
}
Midhat
fuente

Respuestas:

22

Expandiendo la respuesta de David con quien estoy totalmente de acuerdo con que debes crear un contenedor para Random. Escribí más o menos la misma respuesta al respecto anteriormente en una pregunta similar, así que aquí hay una "versión de notas de Cliff".

Lo que debe hacer es crear primero el contenedor como una interfaz (o clase abstracta):

public interface IRandomWrapper {
    int getInt();
}

Y la clase concreta para esto se vería así:

public RandomWrapper implements IRandomWrapper {

    private Random random;

    public RandomWrapper() {
        random = new Random();
    }

    public int getInt() {
        return random.nextInt(10);
    }

}

Digamos que tu clase es la siguiente:

class MyClass {

    public void doSomething() {
        int i=new Random().nextInt(10)
        switch(i)
        {
            //11 case statements
        }
    }

}

Para usar IRandomWrapper correctamente, debe modificar su clase para tomarla como miembro (a través de un constructor o un setter):

public class MyClass {

    private IRandomWrapper random = new RandomWrapper(); // default implementation

    public setRandomWrapper(IRandomWrapper random) {
        this.random = random;
    }

    public void doSomething() {
        int i = random.getInt();
        switch(i)
        {
            //11 case statements
        }
    }

}

Ahora puede probar el comportamiento de su clase con el contenedor, burlándose del contenedor. Puede hacer esto con un marco burlón, pero esto también es fácil de hacer usted mismo:

public class MockedRandomWrapper implements IRandomWrapper {

   private int theInt;    

   public MockedRandomWrapper(int theInt) {
       this.theInt = theInt;
   }

   public int getInt() { 
       return theInt;
   }

}

Dado que su clase espera algo que se parece a una IRandomWrapper, ahora puede usar el simulado para forzar el comportamiento en su prueba. Estos son algunos ejemplos de pruebas JUnit:

@Test
public void testFirstSwitchStatement() {
    MyClass mc = new MyClass();
    IRandomWrapper random = new MockedRandomWrapper(0);
    mc.setRandomWrapper(random);

    mc.doSomething();

    // verify the behaviour for when random spits out zero
}

@Test
public void testFirstSwitchStatement() {
    MyClass mc = new MyClass();
    IRandomWrapper random = new MockedRandomWrapper(1);
    mc.setRandomWrapper(random);

    mc.doSomething();

    // verify the behaviour for when random spits out one
}

Espero que esto ayude.

Spoike
fuente
3
Totalmente de acuerdo con esto. Usted prueba un evento aleatorio eliminando la naturaleza aleatoria del evento. La misma teoría se puede utilizar para las marcas de tiempo
Richard
3
Nota: esta técnica, de dar a un objeto el otro objeto que necesita, en lugar de dejarlo instanciarlos, se llama inyección de dependencia
Clement Herreman
23

Puede (debería) ajustar el código de generación aleatorio en una clase o método y luego simularlo / anularlo durante las pruebas para establecer el valor que desea, de modo que sus pruebas sean predecibles.

David
fuente
5

Tiene un rango específico (0-10) y una granularidad especificada (números enteros). Entonces, cuando se realiza la prueba, no se prueba con los números aleatorios. Usted prueba dentro de un ciclo que golpea cada caso a su vez. Aconsejaría pasar el número aleatorio a una subfunción que contiene la declaración del caso, lo que le permite probar la subfunción.

deworde
fuente
mucho mejor (porque más simple) de lo que sugerí, desearía poder transferir mis votos a favor :)
David
En realidad deberías hacer las dos cosas. Pruebe con un RandomObject simulado para probar cada rama individualmente, y pruebe repetidamente con RandomObject real. La primera es una prueba unitaria, la última más como una prueba de integración.
sleske 03 de
3

Puede usar la biblioteca PowerMock para burlarse de la clase Random y aplicar su método nextInt () para devolver el valor esperado. No es necesario cambiar su código original si no lo desea.

Estoy usando PowerMockito y acabo de probar un método similar al tuyo. Para el código que publicó la prueba JUnit debería verse así:

@RunWith(PowerMockRunner.class)
@PrepareForTest( { Random.class, ClassUsingRandom.class } ) // Don't forget to prepare the Random class! :)

public void ClassUsingRandomTest() {

    ClassUsingRandom cur;
    Random mockedRandom;

    @Before
    public void setUp() throws Exception {

        mockedRandom = PowerMockito.mock(Random.class);

        // Replaces the construction of the Random instance in your code with the mock.
        PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(mockedRandom);

        cur = new ClassUsingRandom();
    }

    @Test
    public void testSwitchAtZero() {

        PowerMockito.doReturn(0).when(mockedRandom).nextInt(10);

        cur.doSomething();

        // Verify behaviour at case 0
     }

    @Test
    public void testSwitchAtOne() {

        PowerMockito.doReturn(1).when(mockedRandom).nextInt(10);

        cur.doSomething();

        // Verify behaviour at case 1
     }

    (...)

También puede desactivar la llamada nextInt (int) para recibir cualquier parámetro, en caso de que desee agregar más casos en su conmutador:

PowerMockito.doReturn(0).when(mockedRandom).nextInt(Mockito.anyInt());

Bonita, ¿no es así? :)

LizardCZ
fuente
2

Use QuickCheck ! Acabo de empezar a jugar con esto recientemente y es increíble. Como la mayoría de las ideas geniales, proviene de Haskell, pero la idea básica es que, en lugar de dar su prueba de casos de prueba preestablecidos, deja que su generador de números aleatorios los construya por usted. De esa manera, en lugar de los 4-6 casos que probablemente se le ocurran en xUnit, puede hacer que la computadora pruebe cientos o miles de entradas y vea cuáles no se ajustan a las reglas que ha establecido.

Además, QuickCheck intentará simplificarlo cuando encuentre un caso que falla para que pueda encontrar el caso más simple que falle. (Y, por supuesto, cuando encuentre un caso fallido, también puede construirlo en una prueba xUnit)

Parece que hay al menos dos versiones para Java, por lo que esa parte no debería ser un problema.

Zachary K
fuente