Jak zrobić TDD dla czegoś z wieloma permutacjami?

15

Podczas tworzenia systemu takiego jak sztuczna inteligencja, który może bardzo szybko podążać wieloma różnymi ścieżkami lub w rzeczywistości dowolnym algorytmem, który ma kilka różnych danych wejściowych, możliwy zestaw wyników może zawierać dużą liczbę permutacji.

Jakie podejście należy zastosować do korzystania z TDD podczas tworzenia systemu, który generuje wiele, wiele różnych kombinacji wyników?

Nicole
źródło
1
Ogólna dobroć systemu AI jest zwykle mierzona za pomocą testu Precision-Recall z zestawem danych wejściowych testu porównawczego. Ten test jest mniej więcej na poziomie „testów integracyjnych”. Jak wspomnieli inni, bardziej przypomina to „badanie algorytmiczne oparte na testach” niż „ projektowanie oparte na testach ”.
rwong
Proszę zdefiniować, co rozumiesz przez „AI”. Jest to dziedzina nauki bardziej niż jakikolwiek konkretny rodzaj programu. W przypadku niektórych implementacji sztucznej inteligencji zasadniczo nie można testować niektórych rodzajów rzeczy (np. Zachowań wschodzących) za pośrednictwem TDD.
Steven Evers
@SnOrfus Mam na myśli w najbardziej ogólnym, szczątkowym sensie, maszynę do podejmowania decyzji.
Nicole

Odpowiedzi:

7

Bardziej praktyczne podejście do odpowiedzi pdr . TDD polega raczej na projektowaniu oprogramowania niż na testowaniu. Testy jednostkowe służą do weryfikacji pracy w miarę upływu czasu.

Tak więc na poziomie testu jednostkowego musisz zaprojektować jednostki, aby mogły być testowane w całkowicie deterministyczny sposób. Możesz to zrobić, usuwając wszystko, co sprawia, że ​​jednostka nie jest deterministyczna (np. Generator liczb losowych), i usuwając to. Powiedzmy, że mamy naiwny przykład metody decydującej, czy ruch jest dobry, czy nie:

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

Ta metoda jest bardzo trudna do przetestowania, a jedyne, co naprawdę można zweryfikować w testach jednostkowych, to jej granice ... ale to wymaga wielu prób dotarcia do granic. Zamiast tego streśćmy losową część, tworząc interfejs i konkretną klasę, która otacza funkcjonalność:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

DeciderKlasa musi teraz korzystać z betonu klasy poprzez abstrakcję, czyli interfejsu. Ten sposób robienia rzeczy nazywa się wstrzykiwaniem zależności (poniższy przykład jest przykładem wstrzykiwania konstruktora, ale można to również zrobić za pomocą settera):

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

Możesz zadać sobie pytanie, dlaczego ten „nadęty kod” jest konieczny. Cóż, na początek, możesz teraz kpić z zachowania losowej części algorytmu, ponieważ Deciderteraz ma zależność, która następuje po IRandom„kontrakcie”. Możesz użyć do tego frameworku, ale ten przykład jest wystarczająco prosty do samodzielnego kodowania:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

Najlepsze jest to, że może to całkowicie zastąpić „faktyczne” konkretne wdrożenie. Kod można łatwo przetestować w następujący sposób:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

Mam nadzieję, że daje to pomysły na zaprojektowanie aplikacji, dzięki czemu można wymusić permutacje, aby można było przetestować wszystkie przypadki brzegowe i tak dalej.

Łup
źródło
3

Ścisłe TDD często psuje się w przypadku bardziej złożonych systemów, ale nie ma to większego znaczenia w praktyce - kiedy wyjdziesz poza możliwość izolacji pojedynczych wejść, po prostu wybierz kilka przypadków testowych, które zapewniają rozsądny zasięg i wykorzystaj je.

Wymaga to pewnej wiedzy na temat tego, co wdrożenie będzie robić dobrze, ale jest to bardziej kwestia teoretyczna - jest mało prawdopodobne, aby budowanie sztucznej inteligencji zostało szczegółowo określone przez użytkowników nietechnicznych. Jest to ta sama kategoria, co przekazywanie testów na stałe do przypadków testowych - oficjalnie test jest specyfikacją, a implementacja jest zarówno poprawna, jak i najszybsze możliwe rozwiązanie, ale tak naprawdę nigdy się nie zdarza.

Tom Clarkson
źródło
2

TDD nie polega na testowaniu, ale na projektowaniu.

Daleko od rozpadu złożoności, wyróżnia się w tych okolicznościach. Zaprowadzi Cię to do rozważenia większego problemu w mniejszych częściach, co doprowadzi do lepszego projektu.

Nie próbuj testować każdej permutacji twojego algorytmu. Po prostu buduj test po teście, napisz najprostszy kod, aby test działał, dopóki nie obejmiesz swoich podstaw. Powinieneś zobaczyć, co mam na myśli mówiąc o rozwiązaniu problemu, ponieważ będziesz zachęcany do udawania części problemu podczas testowania innych części, aby zaoszczędzić sobie konieczności pisania 10 miliardów testów dla 10 miliardów permutacji.

Edycja: Chciałem dodać przykład, ale nie miałem czasu wcześniej.

Rozważmy algorytm sortowania na miejscu. Możemy napisać testy, które obejmują górny koniec tablicy, dolny koniec tablicy i wszelkiego rodzaju dziwne kombinacje w środku. Dla każdego z nich musielibyśmy zbudować pełną tablicę jakiegoś rodzaju obiektu. To zajmie trochę czasu.

Lub możemy rozwiązać problem w czterech częściach:

  1. Przejdź przez tablicę.
  2. Porównaj wybrane elementy.
  3. Zamień przedmioty.
  4. Koordynuj powyższe trzy.

Pierwszy jest jedyną skomplikowaną częścią problemu, ale poprzez oderwanie go od reszty sprawiłeś, że jest on o wiele, wiele prostszy.

Drugi jest prawie na pewno obsługiwany przez sam obiekt, przynajmniej opcjonalnie, w wielu strukturach o typie statycznym dostępny będzie interfejs pokazujący, czy ta funkcjonalność jest zaimplementowana. Więc nie musisz tego testować.

Trzeci jest niezwykle łatwy do przetestowania.

Czwarty obsługuje tylko dwa wskaźniki, prosi klasę ruchu o przesunięcie wskaźników, wzywa do porównania, a na podstawie wyniku tego porównania wzywa do zamiany przedmiotów. Jeśli podrobiłeś pierwsze trzy problemy, możesz to bardzo łatwo przetestować.

Jak udało nam się tutaj stworzyć lepszy projekt? Powiedzmy, że uprościłeś to i wdrożyłeś sortowanie bąbelkowe. Działa, ale kiedy idziesz do produkcji i musi obsłużyć milion obiektów, jest o wiele za wolny. Wszystko, co musisz zrobić, to napisać nową funkcję przejścia i zamienić ją. Nie musisz radzić sobie ze złożonością obsługi pozostałych trzech problemów.

Przekonasz się, że jest to różnica między testowaniem jednostkowym a TDD. Tester jednostkowy powie, że sprawiło to, że twoje testy stały się kruche, a jeśli przetestowałeś proste dane wejściowe i wyjściowe, nie będziesz musiał pisać więcej testów dotyczących nowej funkcjonalności. TDDer powie, że odpowiednio rozdzieliłem obawy, aby każda klasa, którą mam, zrobiła jedną rzecz i jedną rzecz dobrze.

pdr
źródło
1

Nie można przetestować każdej permutacji obliczeń z wieloma zmiennymi. Ale to nic nowego, zawsze tak było w przypadku każdego programu o złożoności zabawkowej. Celem testów jest sprawdzenie właściwości obliczeń. Na przykład sortowanie listy zawierającej 1000 liczb wymaga pewnego wysiłku, ale każde indywidualne rozwiązanie można bardzo łatwo zweryfikować. Teraz, mimo że jest ich 1000! możliwe (klasy) dane wejściowe dla tego programu i nie można ich wszystkich przetestować, wystarczy całkowicie wygenerować 1000 danych losowych i sprawdzić, czy dane wyjściowe są rzeczywiście posortowane. Dlaczego? Ponieważ prawie niemożliwe jest napisanie programu, który niezawodnie sortuje 1000 losowo wygenerowanych wektorów bez ogólnej poprawności (chyba że celowo zmusisz go do manipulowania pewnymi magicznymi danymi wejściowymi ...)

Teraz ogólnie rzeczy są nieco bardziej skomplikowane. Tam naprawdę nie było błędów, gdy program pocztowy nie wyda e-maili do użytkowników, jeśli mają one „F” w jego nazwę i dzień tygodnia jest piątek. Ale uważam, że to zmarnowany wysiłek, próbując przewidzieć taką dziwność. Zestaw testowy powinien zapewnić ci pewność, że system robi to, czego oczekujesz od oczekiwanych danych wejściowych. Jeśli w niektórych funky działa funky, zauważysz to wkrótce po wypróbowaniu pierwszej funky, a następnie możesz napisać test specjalnie na tę sprawę (który zwykle obejmuje również całą klasę podobnych przypadków).

Kilian Foth
źródło
Biorąc pod uwagę, że generujesz losowo 1000 danych wejściowych, w jaki sposób następnie testujesz dane wyjściowe? Z pewnością taki test będzie wymagał pewnej logiki, która sama w sobie nie jest testowana. Więc testujesz test? W jaki sposób? Chodzi o to, że powinieneś przetestować logikę przy użyciu przejść stanu - przy danych wejściowych X wyjście powinno mieć wartość Y. Test obejmujący logikę jest podatny na błędy tak samo, jak logika, którą testuje. Logicznie uzasadnienie argumentu innym argumentem stawia cię na sceptycznej ścieżce regresu - musisz poczynić pewne twierdzenia. Te twierdzenia są twoimi testami.
Izhaki,
0

Weź skrzynie krawędzi i trochę losowych danych wejściowych.

Aby wziąć przykład sortowania:

  • Posortuj kilka losowych list
  • Weź listę, która jest już posortowana
  • Weź listę w odwrotnej kolejności
  • Weź listę, która jest prawie posortowana

Jeśli to działa szybko, możesz być całkiem pewien, że będzie działać dla wszystkich danych wejściowych.

Carra
źródło