Wątek przeniesiony 2022-08-07 14:51 z Nietuzinkowe tematy przez cerrato.

Czy testy mogą się opierać o losowość?

0

Chcę sobie przetestować metodę get_random_cośtam, która zwraca cośtam ze zbioru {cośtam1, cośtam2, cośtam3}. Wpadł mi pomysł do głowy, by zrobić powiedzmy N * 10 prób (gdzie N to wielkość zbioru) podczas których pobieram wynik funkcji i wkładam do zbioru. Po tych N * 10 próbach, powinny być w zbiorze wszystkie możliwe wartości. Czy to podejście ma sens? Samą liczbę N * 10 wziąłem z czapy (w moim teście są 4 możliwe wartości więc po 40 każda powinna się pojawić), ale może istnieje jakieś bardziej szeroko stosowane podejście / liczba?

PS. Chodzi mi oczywiście o to, by test był jak najbardziej deterministyczny i by nie zdarzało się, że będzie failować (ale nie musi być 100% gwarancji)

8
gvim napisał(a):

PS. Chodzi mi oczywiście o to, by test był jak najbardziej deterministyczny i by nie zdarzało się, że będzie failować (ale nie musi być 100% gwarancji)

chcesz celowo robić flaky testy? to nie jest dobre rozwiązanie, bo prowadzi w ogólności do zaniedbywania testów. z drugiej strony, zależy jak często padają testy. jeśli raz na kilka miesięcy to pewnie da się to zaakceptować. w twoim przypadku zmieniłbym podejście i np. zamiast robić stałą liczbę prób za każdym razem to bym losował kolejne elementy, aż pojawią się wszystkie możliwości. w ten sposób w typowym przypadku test działałby krótko, ale przy wyjątkowym pechu mógłby działać długo.

co do losowości samych testowanych danych to pamiętaj, że w testach ważne jest, by dało się zreprodukować problem. jeśli test pada to zrób tak, żeby łatwo było to odtworzyć, a więc np. jeśli masz generator liczb pseudolosowych to wylosuj random seed wprost na początku testu, zapis go gdzieś tak, żeby dało się go wyczytać z wyników testu i ponadto odizoluj ten generator liczb pseudolosowych, tzn. w każdym teście używaj oddzielnego po to, by kolejność odpalania testów nie miała znaczenia.

4

(w moim teście są 4 możliwe wartości więc po 40 każda powinna się pojawić)

Statystycznie tak, ale równie dobrze przez 150 losowań możesz mieć zawsze tylko opcję numer 1 i 2, a 4 nie pojawi się nigdy. Prawdopodobnie, jeśli masz 4 opcje to po 40 losowaniach każda z nich się pojawi, ale pewności 100% nie możesz mieć. A co za tym idzie - jeśli przy testach założysz, że na pewno każda wartość się wylosuje, to masz błędne założenie i możesz przez to mieć zakłamane wyniki testów.

No i zgadzam się z @Wibowit - jeśli chcesz robić testy, to osobno bym testował sam mechanizm losujący, a osobno/niezależnie procedurę, która coś robi na podstawie tych danych losowych. Wtedy do testów procedury dajesz te same "losowe" wartości (mogą być one nawet rzeczywiście wylosowane wcześniej i gdzieś zapisane) i przy takich samych danych wejściowych, powinieneś dostawać zawsze takie same efekty.

2

Kiedyś dostałem jakąś książkę z zagadkami matematycznymi dla dzieci. Był tam taki przykład, że gość policzył sobie, że skoro w populacji liczba kobiet i mężczyzn jest taka sama, to prawdopodobieństwo, że kolejne 20 osób będzie kobietami to 1:2^20, znalazł frajera, którzy zgodził się z nim założyć, a później ulicą przeszła drużyna harcerek....
Prawdopodobieństwo sprawdzi się przy nieskończonej liczbie prób. Jeżeli masz listę 10 elementów, losujesz z niej 10 razy i sprawdzasz, czy w wynikach jest wszystko, to raczej rzadko taki test przejdzie. Policzmy:
10.90.80.70.60.50.40.30.2*0.1 = 0.00036288, czyli test przejdzie ci raz na 2756 razy.

Jeżeli masz 4 elementy i losujesz 40 razy, to prawdopodobieństwo, że nie wylosujesz pierwszego elementu listy, to (3/4) ^ 40, czyli ~1:100000, może się spełnić dla każdego elementu, czyli raz na 25000 prób coś wyskoczy i nawet nie będziesz wiedział, czy to błąd, czy "stało się"

Nie lepiej wstrzyknąć ziarno generatora losowego do testu i mieć zawsze taki sam wynik?

2

Tak, ale prawdopodobieństwo powinno być tak niskie, że niepowodzenie jest okazją do świętowania, bo wydarzyło się coś niespodziewanego.

Po tych N * 10 próbach, powinny być w zbiorze wszystkie możliwe wartości. Czy to podejście ma sens?

Policz jakie jest prawdopodobieństwo niepowodzenia dla najgorszego przypadku i idealnego algorytmu. Jak jest mniejsze niż astronomiczne sumy typu liczba atomów we wszechświecie to starałbym się zwiększyć liczbę prób.

1

Takie testy nie mają sensu, bo jak przewidzisz dla losowych wartości wynik funkcji?

musisz jeszcze raz napisać funkcję, którą testujesz lub mieć funkcję z innej biblioteki, która jest sprawdzona i to wykona za ciebie, czyli po co ty ją implementowałeś jak masz perfekcyjną funkcję?
W testach dajesz jakiś problem i znasz jego rozwiązanie dlatego możesz łatwo corner casy sprawdzić, jakiegoś buga zreprodukować bo wiesz jak powinno się zachować, a jak bug się zachował zupełnie inaczej od przewidywanych wartości.

Testowanie randomowo to bardziej pod fuzzing pochodzi gdzie losowo zmieniasz bity i czekasz aż się program wywali i potem analizujesz jakie execeptiony cpu zostały wyzwolone, czasem jakieś prowadzą do błędów, które mogą być nadużyte.

4

Oczywiście, że można używać (ograniczonej) losowości w testach, bardzo często sam to stosuję i to się nazywa property testing. Oczywiście trzeba takie testy umieć pisać, a przypadek jaki podałeś oczywiście, że może nie mieć miejsca. W twoim przypadku test powinien wyglądać mniej więcej tak:

check all list <- list_of(any(), min_length: 1) do
  assert pick_random(list) in list
end

Czyli sprawdzamy czy dla dowolnej listy z przynajmniej jednym elementem wylosowany element znajduje się w naszej liście. To jest niezmiennik, który zawsze musi być spełniony. Niezależnie od tego jak bardzo poszalejemy z losowością, zawsze wybrany element musi być elementem oryginalnej listy.

6

Ze względu na powtarzalność raczej nie ma miejsca na losowośc. Ale użycie generatora liczb pseudolosowych z zadanym seedem to normalka i zwykle wystarcza.

3

Moim zdaniem to jest słaby pomysł, z kilku powodów.

  • Po pierwsze, powtarzalność testów - i nie sądzę że ustalenie seeda jest tutaj wyjściem, bo jak się zmieni zakres tych wartości (np jak ktoś zmieni 1-100 na 1-120) to wszystkie wartości już będą inne. Także, owszem powtarzalność testów jest zachowana z seedem, ale tak długo jak nie edytujesz kodu.
  • Po drugie, to wygląda jakbyś nie wiedział co testujesz, nie masz pomysłu jak napisać dobry test, więc odpalasz generator liczb losowych, licząc na uniform distribution i na to że to wychwyci Ci wszystkie opcje albo wszystkie przypadki brzegowe.
  • Po trzecie, jak czytam test, to chciałbym wiedzieć że ktoś go napisał po coś. Że ten test wnosi jakąś wartość, prezentuje jakiś element systemu. Jak widzę randoma, to nie wiem po co on jest. Owszem, testy mają sprawdzić czy system nadaje się do wdrożenia, ale muszą też być żywą specyfikacją jeśli mają być utrzymywalne.

Innymi słowy: nie sądzę że jest przypadek kiedy losowe testy dostarczają czegokolwiek, czego nie mogą dostarczyć zwykłe testy które nie mają w sobie losowości, a za to mają inne zalety, np czytelność.

0

@Riddle: To jak byś zrobił test metody zwracającej sumę rzutu 5 kośćmi do gry? Zakres wartości 5..30, rozkład ~normalny, czyli skrajne wartości mają prawdopodobieństwo 1/7776, środkowej mi się nie chce liczyć. Test ma swoje uzasadnienie biznesowe i jest nim jakaś tam gra losowa.
--edit

class DiceCup{
val random = Random()
  fun roll():Int{
    var result = 0
    repat(5){
      result += random.nextInt(5)+1
    }
    return result
  }
}
1
piotrpo napisał(a):

@Riddle: To jak byś zrobił test metody zwracającej sumę rzutu 5 kośćmi do gry? Zakres wartości 5..30, rozkład ~normalny, czyli skrajne wartości mają prawdopodobieństwo 1/7776, środkowej mi się nie chce liczyć. Test ma swoje uzasadnienie biznesowe i jest nim jakaś tam gra losowa.
--edit

class DiceCup{
val random = Random()
  fun roll():Int{
    var result = 0
    repat(5){
      result += random.nextInt(5)+1
    }
    return result
  }
}

Mniej więcej tak

fun testRoll() {
  val cup = DiceCup(SequentialRandom(4,3,2,4,2))
  assertEquals(cup.roll(), 4+3+2+4+2); // 15
}
fun testFiveOnes() {
  val cup = DiceCup(ConstantRandom(1))
  assertEquals(cup.roll(), 5);
}
fun testFiveFives() {
  val cup = DiceCup(ConstantRandom(5))
  assertEquals(cup.roll(), 25);
}

Oczywiście SequentialRandom extends Random oraz ConstantRandom extends Random, to po prostu fake klasy.

0

Zastanawiam się nad tym jaka jest przewaga wstrzykiwania fakowego Random{}, nad wstrzykiwaniem opcjonalnego seeda. Wiem, że wynik sumowania można łatwo przewidzieć pisząc test, z drugiej strony pomija się w ten sposób istotę tej klasy, czyli zwracanie losowych wyników o określonym rozkładzie.

1
gvim napisał(a):

Chcę sobie przetestować metodę get_random_cośtam, która zwraca cośtam ze zbioru {cośtam1, cośtam2, cośtam3}. Wpadł mi pomysł do głowy, by zrobić powiedzmy N * 10 prób (gdzie N to wielkość zbioru) podczas których pobieram wynik funkcji i wkładam do zbioru. Po tych N * 10 próbach, powinny być w zbiorze wszystkie możliwe wartości. Czy to podejście ma sens? Samą liczbę N * 10 wziąłem z czapy (w moim teście są 4 możliwe wartości więc po 40 każda powinna się pojawić), ale może istnieje jakieś bardziej szeroko stosowane podejście / liczba?

PS. Chodzi mi oczywiście o to, by test był jak najbardziej deterministyczny i by nie zdarzało się, że będzie failować (ale nie musi być 100% gwarancji)

No to ja Ci proponuję, wydziel klasę która zwraca losowe wartości, a potem wsadź jej fake'ową wersję w teście klasy którą chcesz testować.

0

Mogą, ale wtedy nie mają sensu.

Czy dom może nie mieć fundamentów? Może, ale ja do niego nie wchodzę.

1 użytkowników online, w tym zalogowanych: 0, gości: 1