Como criar casos de teste para cobrir o código com base em eventos aleatórios?

15

Por exemplo, se o código gera um int aleatório de 0 a 10 e usa uma ramificação diferente em cada resultado, como projetar um conjunto de testes para garantir 100% de cobertura de instrução nesse código?

Em Java, o código pode ser algo como:

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

Respostas:

22

Expandindo a resposta de David, com a qual concordo totalmente que você deve criar um invólucro para o Random. Eu escrevi praticamente a mesma resposta sobre isso anteriormente em uma pergunta semelhante, então aqui está uma "versão das notas de Cliff".

O que você deve fazer é primeiro criar o wrapper como uma interface (ou classe abstrata):

public interface IRandomWrapper {
    int getInt();
}

E a classe concreta para isso ficaria assim:

public RandomWrapper implements IRandomWrapper {

    private Random random;

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

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

}

Digamos que sua turma seja a seguinte:

class MyClass {

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

}

Para usar o IRandomWrapper corretamente, você precisa modificar sua classe para aceitá-la como membro (por meio de construtor ou 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
        }
    }

}

Agora você pode testar o comportamento da sua classe com o wrapper, zombando do wrapper. Você pode fazer isso com uma estrutura de simulação, mas isso também é fácil de fazer:

public class MockedRandomWrapper implements IRandomWrapper {

   private int theInt;    

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

   public int getInt() { 
       return theInt;
   }

}

Como sua classe espera algo parecido com um, IRandomWrapperagora você pode usar o que é ridicularizado para forçar o comportamento em seu teste. Aqui estão alguns exemplos de testes 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 isto ajude.

Spoike
fonte
3
Concordo totalmente com isso. Você testa um evento aleatório removendo a natureza aleatória do evento. A mesma teoria pode ser usado para timestamps
Richard
3
Nota: este tehcnique, de dar um objeto a outro objeto que precisa, em vez de deixá-lo instanciar-los, é chamado Dependency Injection
Clement Herreman
23

Você pode (deve) agrupar o código de geração aleatória em uma classe ou método e depois simular / substituir durante os testes para definir o valor desejado, para que seus testes sejam previsíveis.

David
fonte
5

Você tem um intervalo especificado (0-10) e uma granularidade especificada (números inteiros). Portanto, ao testar, você não testa com números aleatórios. Você testa dentro de um loop que atinge cada caso por vez. Aconselho passar o número aleatório para uma subfunção que contém a instrução case, que permite testar apenas a subfunção.

Deworde
fonte
muito melhor (porque mais simples) do que o que eu sugeri, gostaria de poder transferir meus upvotes :)
David
Na verdade você deve fazer as duas coisas. Teste com um RandomObject falso para testar cada ramificação individualmente e teste repetidamente com o RandomObject real. O primeiro é um teste de unidade, o último mais como um teste de integração.
sleske
3

Você pode usar a biblioteca do PowerMock para zombar da classe Random e stub seu método nextInt () para retornar o valor esperado. Não é necessário alterar o código original, se você não quiser.

Estou usando o PowerMockito e acabei de testar um método semelhante ao seu. Para o código que você postou o teste JUnit deve ser algo como isto:

@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
     }

    (...)

Você também pode stub a chamada nextInt (int) para receber qualquer parâmetro, caso queira adicionar mais casos ao seu switch:

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

Bonito, não é? :)

LizardCZ
fonte
2

Use o QuickCheck ! Eu comecei a brincar com isso recentemente e é incrível. Como a maioria das idéias interessantes, ela vem da Haskell, mas a idéia básica é que, em vez de fornecer aos seus casos de teste pré-enlatados, você deixe seu gerador de números aleatórios construí-los para você. Dessa forma, em vez dos 4-6 casos que você provavelmente apresentaria no xUnit, o computador pode tentar centenas ou milhares de entradas e ver quais não estão em conformidade com as regras que você definiu.

O QuickCheck também tentará simplificá-lo, quando encontrar um caso com falha, para encontrar o caso mais simples possível que falhar. (E, é claro, quando você encontra um caso com falha, pode construí-lo também em um teste xUnit)

Parece haver pelo menos duas versões para Java, para que parte não deva ser um problema.

Zachary K
fonte