Mój obecny projekt, zwięźle, polega na tworzeniu „zdarzeń losowo możliwych”. Generalnie generuję harmonogram inspekcji. Niektóre z nich opierają się na ścisłych harmonogramach; raz w tygodniu przeprowadzasz kontrolę w piątek o godzinie 10:00. Inne kontrole są „losowe”; istnieją podstawowe konfigurowalne wymagania, takie jak „inspekcja musi odbywać się 3 razy w tygodniu”, „inspekcja musi odbywać się między godzinami 9–21” i „nie powinny być dwie inspekcje w tym samym okresie 8 godzin”, ale niezależnie od ograniczeń skonfigurowanych dla określonego zestawu inspekcji, wynikające z nich daty i godziny nie powinny być przewidywalne.
Testy jednostkowe i TDD, IMO, mają wielką wartość w tym systemie, ponieważ można je wykorzystać do stopniowego budowania go, podczas gdy jego pełny zestaw wymagań jest wciąż niekompletny, i upewnij się, że nie jestem „nadmiernie inżynieryjny”, aby robić rzeczy, które nie daję obecnie wiem, że potrzebuję. Surowe harmonogramy były dla TDD bułką z masłem. Jednak trudno mi zdefiniować, co testuję, pisząc testy dla losowej części systemu. Mogę stwierdzić, że wszystkie czasy generowane przez program planujący muszą mieścić się w ograniczeniach, ale mógłbym zaimplementować algorytm, który przejdzie wszystkie takie testy bez faktycznego czasu bardzo „losowego”. W rzeczywistości tak właśnie się stało; Znalazłem problem, w którym czasy, choć nie do końca dokładnie przewidywalne, mieściły się w niewielkim podzbiorze dopuszczalnych zakresów daty / czasu. Algorytm nadal spełniał wszystkie stwierdzenia, które według mnie mógłbym rozsądnie sformułować, i nie mogłem zaprojektować zautomatyzowanego testu, który zawiódłby w tej sytuacji, ale przejść, gdy otrzyma „bardziej losowe” wyniki. Musiałem wykazać, że problem został rozwiązany przez restrukturyzację niektórych istniejących testów, aby powtórzyć się wiele razy i wizualnie sprawdzić, czy generowane czasy mieszczą się w pełnym dopuszczalnym zakresie.
Czy ktoś ma jakieś wskazówki dotyczące projektowania testów, które powinny oczekiwać zachowania niedeterministycznego?
Dziękujemy wszystkim za sugestie. Głównym opinia wydaje się, że muszę deterministycznego testu w celu uzyskania deterministyczne i powtarzalne wyniki, assertable . Ma sens.
Stworzyłem zestaw testów „piaskownicy”, które zawierają algorytmy kandydujące do procesu ograniczania (proces, w którym tablica bajtów, która może być dowolna, staje się długa między min a maksimum). Następnie uruchamiam ten kod przez pętlę FOR, która daje algorytmowi kilka znanych tablic bajtowych (wartości od 1 do 10 000 000 na początek) i ma algorytm ograniczający do wartości od 1009 do 7919 (używam liczb pierwszych, aby zapewnić algorytm nie przepuściłby jakiegoś nieoczekiwanego GCF między zakresami wejściowym i wyjściowym). Wynikowe ograniczone wartości są zliczane i generowany histogram. Aby „przejść”, wszystkie dane wejściowe muszą zostać odzwierciedlone w histogramie (rozsądek, aby upewnić się, że nie „straciliśmy” żadnego), a różnica między dowolnymi dwoma segmentami w histogramie nie może być większa niż 2 (tak naprawdę powinna wynosić <= 1 , ale bądźcie czujni). Zwycięski algorytm, jeśli taki istnieje, można wyciąć i wkleić bezpośrednio w kodzie produkcyjnym, a także przeprowadzić stały test regresji.
Oto kod:
private void TestConstraintAlgorithm(int min, int max, Func<byte[], long, long, long> constraintAlgorithm)
{
var histogram = new int[max-min+1];
for (int i = 1; i <= 10000000; i++)
{
//This is the stand-in for the PRNG; produces a known byte array
var buffer = BitConverter.GetBytes((long)i);
long result = constraintAlgorithm(buffer, min, max);
histogram[result - min]++;
}
var minCount = -1;
var maxCount = -1;
var total = 0;
for (int i = 0; i < histogram.Length; i++)
{
Console.WriteLine("{0}: {1}".FormatWith(i + min, histogram[i]));
if (minCount == -1 || minCount > histogram[i])
minCount = histogram[i];
if (maxCount == -1 || maxCount < histogram[i])
maxCount = histogram[i];
total += histogram[i];
}
Assert.AreEqual(10000000, total);
Assert.LessOrEqual(maxCount - minCount, 2);
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionMSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByMSBRejection);
}
private long ConstrainByMSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length-1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Apply a bitmask to the value, removing the MSB on each loop until it falls in the range.
var mask = long.MaxValue;
while (result > max - min)
{
mask >>= 1;
result &= mask;
}
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionLSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByLSBRejection);
}
private long ConstrainByLSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length - 1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Bit-shift the number 1 place to the right until it falls within the range
while (result > max - min)
result >>= 1;
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionModulus()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByModulo);
}
private long ConstrainByModulo(byte[] buffer, long min, long max)
{
buffer[buffer.Length - 1] &= 0x7f;
var result = BitConverter.ToInt64(buffer, 0);
//Modulo divide the value by the range to produce a value that falls within it.
result %= max - min + 1;
result += min;
return result;
}
... a oto wyniki:
Odrzucenie LSB (przesunięcie liczby bitów, aż mieści się w zakresie) było STRASZNE, z bardzo łatwego do wyjaśnienia powodu; dzieląc dowolną liczbę przez 2, aż będzie ona mniejsza niż maksimum, rezygnujesz z niej natychmiast, a dla każdego nietrywialnego zakresu spowoduje to przesunięcie wyników w kierunku górnej jednej trzeciej (jak pokazano w szczegółowych wynikach histogramu ). Takie było dokładnie zachowanie, które widziałem od daty ukończenia; wszystkie czasy były po południu, w bardzo konkretne dni.
Odrzucenie MSB (usuwanie najbardziej znaczącego bitu z liczby pojedynczo, aż znajdzie się w zakresie) jest lepsze, ale znowu, ponieważ odcinasz bardzo duże liczby z każdym bitem, nie jest on równomiernie rozłożony; jest mało prawdopodobne, aby uzyskać liczby w górnym i dolnym końcu, więc otrzymujesz odchylenie w kierunku środkowej trzeciej. Może to przydać się komuś, kto chce „znormalizować” losowe dane w krzywą dzwonową, ale suma dwóch lub więcej mniejszych liczb losowych (podobnych do rzucania kostkami) dałaby bardziej naturalną krzywą. Dla moich celów zawodzi.
Jedynym, który zdał ten test, było ograniczenie przez podział modulo, który również okazał się najszybszy z trzech. Z definicji Modulo będzie wytwarzać możliwie równomierny rozkład, biorąc pod uwagę dostępne dane wejściowe.