Jak zaprojektować przypadki testowe do pokrycia kodu na podstawie zdarzeń losowych?

15

Na przykład, jeśli kod generuje losową liczbę całkowitą od 0-10 i przyjmuje inną gałąź dla każdego wyniku, jak można zaprojektować zestaw testowy, aby zagwarantować 100% pokrycie instrukcji w takim kodzie?

W Javie kod może wyglądać następująco:

int i = new Random().nextInt(10);
switch(i)
{
    //11 case statements
}
Midhat
źródło

Odpowiedzi:

22

Rozszerzając odpowiedź Davida, z którą całkowicie się zgadzam, powinieneś stworzyć opakowanie dla Random. Wcześniej napisałem prawie taką samą odpowiedź w podobnym pytaniu, więc oto „wersja notatek Cliffa”.

Co powinieneś zrobić, to najpierw utworzyć opakowanie jako interfejs (lub klasę abstrakcyjną):

public interface IRandomWrapper {
    int getInt();
}

Konkretna klasa do tego wyglądałaby tak:

public RandomWrapper implements IRandomWrapper {

    private Random random;

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

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

}

Powiedz, że twoja klasa jest następująca:

class MyClass {

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

}

Aby poprawnie korzystać z IRandomWrapper, musisz zmodyfikować swoją klasę, aby przyjmowała ją jako element członkowski (przez konstruktor lub program ustawiający):

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

}

Możesz teraz przetestować zachowanie swojej klasy za pomocą opakowania, wyśmiewając opakowanie. Możesz to zrobić za pomocą fałszywego frameworka, ale jest to również łatwe do zrobienia samemu:

public class MockedRandomWrapper implements IRandomWrapper {

   private int theInt;    

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

   public int getInt() { 
       return theInt;
   }

}

Ponieważ twoja klasa oczekuje czegoś, co wygląda jak IRandomWrapperwy, możesz teraz użyć kpiny, aby wymusić zachowanie w teście. Oto kilka przykładów testów 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
}

Mam nadzieję że to pomoże.

Łup
źródło
3
Całkowicie się z tym zgadzam. Testujesz losowe zdarzenie, usuwając losowy charakter zdarzenia. Tę samą teorię można zastosować do znaczników czasu
Richard
3
Uwaga: ta technika, polegająca na nadaniu przedmiotowi innego potrzebnego mu przedmiotu, zamiast pozwolić mu na jego utworzenie, nazywa się Wstrzykiwaniem Zależności
Clement Herreman
23

Możesz (należy) owinąć losowy kod generujący w klasę lub metodę, a następnie wyśmiać / przesłonić go podczas testów, aby ustawić żądaną wartość, aby testy były przewidywalne.

David
źródło
5

Masz określony zakres (0–10) i określoną ziarnistość (liczby całkowite). Więc podczas testowania nie testujesz liczb losowych. Testujesz w pętli, która trafia kolejno do każdego przypadku. Radziłbym przekazać liczbę losową do podfunkcji zawierającej instrukcję case, która pozwala tylko przetestować podfunkcję.

deworde
źródło
o wiele lepszy (bo prostszy) niż to, co zasugerowałem, szkoda, że ​​nie mogę przenieść moich pozytywnych opinii :)
David
Właściwie powinieneś zrobić jedno i drugie. Przetestuj za pomocą fałszywego obiektu RandomObject, aby przetestować każdą gałąź osobno, a następnie kilkakrotnie przetestuj za pomocą prawdziwego obiektu RandomObject. Pierwszy to test jednostkowy, drugi bardziej przypomina test integracyjny.
śleske,
3

Możesz użyć biblioteki PowerMock, aby wyśmiewać klasę Random i wprowadzić jej metodę nextInt (), aby zwrócić oczekiwaną wartość. Nie musisz zmieniać oryginalnego kodu, jeśli nie chcesz.

Używam PowerMockito i właśnie przetestowałem metodę podobną do twojej. W przypadku kodu, który opublikowałeś, test JUnit powinien wyglądać mniej więcej tak:

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

    (...)

Możesz także pominąć wywołanie nextInt (int), aby otrzymać dowolny parametr, na wypadek, gdybyś chciał dodać więcej przypadków na przełączniku:

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

Ładne, prawda? :)

LizardCZ
źródło
2

Użyj QuickCheck ! Właśnie zacząłem grać z tym niedawno i jest niesamowity. Jak większość fajnych pomysłów, pochodzi od Haskella, ale podstawową ideą jest to, że zamiast dawać testowi gotowe skrzynki testowe, pozwalasz generatorowi liczb losowych zbudować je dla ciebie. W ten sposób zamiast 4-6 przypadków, które prawdopodobnie wymyślisz w xUnit, możesz poprosić komputer o wypróbowanie setek lub tysięcy danych wejściowych i sprawdzenie, które nie są zgodne z ustawionymi regułami.

Również QuickCheck, gdy znajdzie przypadek nieudany, spróbuje go uprościć, aby mógł znaleźć najprostszy możliwy przypadek, który się nie powiedzie. (I oczywiście, gdy znajdziesz niesprawny przypadek, możesz go również wbudować w test xUnit)

Wydaje się, że istnieją co najmniej dwie wersje Java, więc ta część nie powinna stanowić problemu.

Zachary K.
źródło