Comment puis-je concevoir des cas de test pour couvrir du code basé sur des événements aléatoires?

15

Par exemple, si le code génère un entier aléatoire de 0 à 10 et prend une branche différente sur chaque résultat, comment peut-on concevoir une suite de tests pour garantir une couverture à 100% des instructions dans un tel code?

En Java, le code pourrait être quelque chose comme:

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

Réponses:

22

En développant la réponse de David avec qui je suis totalement d'accord avec vous, vous devriez créer un wrapper pour Random. J'ai écrit à peu près la même réponse à ce sujet plus tôt dans une question similaire, alors voici une "version des notes de Cliff".

Ce que vous devez faire est de créer d'abord le wrapper en tant qu'interface (ou classe abstraite):

public interface IRandomWrapper {
    int getInt();
}

Et la classe concrète pour cela ressemblerait à ceci:

public RandomWrapper implements IRandomWrapper {

    private Random random;

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

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

}

Disons que votre classe est la suivante:

class MyClass {

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

}

Afin d'utiliser correctement IRandomWrapper, vous devez modifier votre classe pour la prendre comme membre (via un constructeur ou 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
        }
    }

}

Vous pouvez maintenant tester le comportement de votre classe avec l'encapsuleur, en vous moquant de l'encapsuleur. Vous pouvez le faire avec un cadre de simulation, mais c'est aussi facile à faire par vous-même:

public class MockedRandomWrapper implements IRandomWrapper {

   private int theInt;    

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

   public int getInt() { 
       return theInt;
   }

}

Étant donné que votre classe attend quelque chose qui ressemble à un, IRandomWrappervous pouvez maintenant utiliser celui simulé pour forcer le comportement dans votre test. Voici quelques exemples de tests 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
}

J'espère que cela t'aides.

Spoike
la source
3
Tout à fait d'accord avec cela. Vous testez un événement aléatoire en supprimant la nature aléatoire de l'événement. La même théorie peut être utilisée pour les horodatages
Richard
3
Remarque: cette technique, de donner à un objet l'autre objet dont il a besoin, au lieu de le laisser l'instancier, s'appelle Dependency Injection
Clement Herreman
23

Vous pouvez (devez) encapsuler le code de génération aléatoire dans une classe ou une méthode, puis le simuler / le remplacer pendant les tests pour définir la valeur souhaitée, afin que vos tests soient prévisibles.

David
la source
5

Vous avez une plage spécifiée (0-10) et une granularité spécifiée (nombres entiers). Ainsi, lors des tests, vous ne testez pas avec les nombres aléatoires. Vous testez dans une boucle qui frappe tour à tour chaque cas. Je conseillerais de passer le nombre aléatoire dans une sous-fonction contenant l'instruction case, ce qui vous permet de simplement tester la sous-fonction.

deworde
la source
beaucoup mieux (parce que plus simple) que ce que j'ai suggéré, j'aimerais pouvoir transférer mes votes positifs :)
David
En fait, vous devriez faire les deux. Testez avec un faux RandomObject pour tester chaque branche individuellement, et testez à plusieurs reprises avec de vrais RandomObject. Le premier est un test unitaire, le second ressemble plus à un test d'intégration.
sleske
3

Vous pouvez utiliser la bibliothèque PowerMock pour simuler la classe Random et bloquer sa méthode nextInt () pour renvoyer la valeur attendue. Pas besoin de changer votre code d'origine si vous ne le souhaitez pas.

J'utilise PowerMockito et je viens de tester une méthode similaire à la vôtre. Pour le code que vous avez publié, le test JUnit devrait ressembler à ceci:

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

    (...)

Vous pouvez également bloquer l'appel nextInt (int) pour recevoir n'importe quel paramètre, au cas où vous voudriez ajouter plus de cas à votre commutateur:

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

Joli, non? :)

LizardCZ
la source
2

Utilisez QuickCheck ! Je viens de commencer à jouer avec ça récemment et c'est incroyable. Comme la plupart des idées sympas, cela vient de Haskell, mais l'idée de base est qu'au lieu de donner à vos tests des cas de test prédéfinis, vous laissez votre générateur de nombres aléatoires les construire pour vous. De cette façon, au lieu des 4-6 cas que vous trouveriez probablement dans xUnit, vous pouvez demander à l'ordinateur d'essayer des centaines ou des milliers d'entrées et de voir lesquelles ne sont pas conformes aux règles que vous avez définies.

QuickCheck essayera également de le simplifier lorsqu'il trouvera un cas défaillant afin qu'il puisse trouver le cas le plus simple possible qui échoue. (Et bien sûr, lorsque vous trouvez un cas défaillant, vous pouvez également le créer dans un test xUnit)

Il semble y avoir au moins deux versions pour Java, donc cette partie ne devrait pas poser de problème.

Zachary K
la source