Wie mache ich TDD für etwas mit vielen Permutationen?


15

Wenn Sie ein System wie eine KI erstellen, die sehr schnell viele verschiedene Pfade einschlagen kann, oder einen Algorithmus mit mehreren verschiedenen Eingaben, kann die mögliche Ergebnismenge eine große Anzahl von Permutationen enthalten.

Welchen Ansatz sollte man wählen, um TDD zu verwenden, wenn man ein System erstellt, das viele, viele verschiedene Permutationen von Ergebnissen ausgibt?


1
Die Gesamtgüte des AI-Systems wird normalerweise durch einen Precision-Recall-Test mit einem Benchmark-Eingabesatz gemessen. Dieser Test entspricht in etwa den "Integrationstests". Wie andere bereits erwähnt haben, handelt es sich eher um "testgetriebene Algorithmusforschung" als um "testgetriebenes Design ".
rwong

Bitte definieren Sie, was Sie mit "AI" meinen. Es ist mehr ein Studienbereich als eine bestimmte Art von Programm. Bei bestimmten AI-Implementierungen können Sie im Allgemeinen nicht über TDD auf bestimmte Arten von Dingen testen (z. B. auftauchendes Verhalten).
Steven Evers

@SnOrfus Ich meine es im allgemeinsten, rudimentären Sinne, eine Entscheidungsmaschine.
Nicole

Antworten:


7

Einen praktischeren Ansatz für die Antwort von pdr wählen . Bei TDD geht es eher um Software-Design als um Testen. Sie verwenden Unit-Tests, um Ihre Arbeit zu überprüfen.

Auf der Ebene der Einheitentests müssen Sie die Einheiten so entwerfen, dass sie vollständig deterministisch getestet werden können. Sie können dies tun, indem Sie alles nehmen, was die Einheit nicht deterministisch macht (wie z. B. einen Zufallszahlengenerator), und das abstrahieren. Nehmen wir an, wir haben ein naives Beispiel für eine Methode, die entscheidet, ob ein Zug gut ist oder nicht:

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);

Diese Methode ist sehr schwer zu testen und das einzige, was Sie in Unit-Tests wirklich überprüfen können, sind die Grenzen ... aber das erfordert eine Menge Versuche, um an die Grenzen zu gelangen. Lassen Sie uns stattdessen den Randomisierungsteil abstrahieren, indem Sie eine Schnittstelle und eine konkrete Klasse erstellen, die die Funktionalität umschließt:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

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

}

Die DeciderKlasse muss nun die konkrete Klasse durch ihre Abstraktion, dh das Interface, verwenden. Diese Vorgehensweise wird als Abhängigkeitsinjektion bezeichnet (das folgende Beispiel ist ein Beispiel für die Konstruktorinjektion, aber Sie können dies auch mit einem Setter tun):

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);

Sie könnten sich fragen, warum dieser "Code aufblähen" notwendig ist. Nun, für den Anfang können Sie jetzt das Verhalten des zufälligen Teils des Algorithmus verspotten, da der Decidernun eine Abhängigkeit hat, die dem IRandoms "Vertrag" folgt . Sie können hierfür ein Mocking-Framework verwenden. Dieses Beispiel ist jedoch einfach genug, um sich selbst zu codieren:

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

}

Das Beste daran ist, dass dies die "tatsächliche" konkrete Implementierung vollständig ersetzen kann. Der Code wird so einfach zu testen:

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

Hoffe, dies gibt Ihnen Anregungen, wie Sie Ihre Anwendung so gestalten können, dass die Permutationen erzwungen werden, sodass Sie alle Randfälle und so weiter testen können.


3

Strict TDD funktioniert bei komplexeren Systemen in der Regel etwas schlechter, aber das ist praktisch nicht so wichtig. Wenn Sie nicht mehr in der Lage sind, einzelne Eingänge zu isolieren, wählen Sie einfach einige Testfälle aus, die eine angemessene Abdeckung bieten, und verwenden Sie diese.

Dies erfordert einige Kenntnisse darüber, wie die Implementierung erfolgreich sein wird. Dies ist jedoch eher ein theoretisches Problem. Es ist sehr unwahrscheinlich, dass Sie eine KI erstellen, die von nicht-technischen Benutzern detailliert spezifiziert wurde. Es ist in der gleichen Kategorie wie das Bestehen von Tests durch Hardcodierung der Testfälle - offiziell ist der Test die Spezifikation und die Implementierung ist sowohl korrekt als auch die schnellstmögliche Lösung, aber es passiert nie wirklich.


2

Bei TDD geht es nicht um Testen, sondern um Design.

Weit davon entfernt, mit der Komplexität auseinanderzufallen, ist es unter diesen Umständen hervorragend. Es wird Sie veranlassen, das größere Problem in kleineren Teilen zu betrachten, was zu einem besseren Design führt.

Versuchen Sie nicht, jede Permutation Ihres Algorithmus zu testen. Erstellen Sie einfach Test für Test und schreiben Sie den einfachsten Code, damit der Test funktioniert, bis Ihre Grundlagen abgedeckt sind. Sie sollten verstehen, was ich mit dem Auflösen des Problems meine, da Sie aufgefordert werden, beim Testen anderer Teile Teile des Problems auszutricksen, damit Sie nicht 10 Milliarden Tests für 10 Milliarden Permutationen schreiben müssen.

Edit: Ich wollte ein Beispiel hinzufügen, hatte aber vorher keine Zeit.

Betrachten wir einen In-Place-Sortieralgorithmus. Wir könnten Tests schreiben, die das obere Ende des Arrays, das untere Ende des Arrays und alle möglichen komischen Kombinationen in der Mitte abdecken. Für jedes müssten wir ein komplettes Array von Objekten erstellen. Das würde Zeit brauchen.

Oder wir könnten das Problem in vier Teilen angehen:

  1. Durchqueren Sie das Array.
  2. Vergleichen Sie ausgewählte Elemente.
  3. Elemente wechseln.
  4. Koordiniere die obigen drei.

Der erste ist der einzig komplizierte Teil des Problems, aber indem Sie es vom Rest abstrahieren, haben Sie es viel, viel einfacher gemacht.

Die zweite Aufgabe wird mit ziemlicher Sicherheit vom Objekt selbst übernommen, zumindest optional. In vielen Frameworks mit statischem Typ gibt es eine Schnittstelle, die anzeigt, ob diese Funktionalität implementiert ist. Sie müssen das also nicht testen.

Der dritte ist unglaublich einfach zu testen.

Der vierte behandelt nur zwei Zeiger, fordert die Traversal-Klasse auf, die Zeiger zu verschieben, fordert einen Vergleich an und fordert auf der Grundlage des Ergebnisses dieses Vergleichs die auszutauschenden Elemente an. Wenn Sie die ersten drei Probleme durchgespielt haben, können Sie dies ganz einfach testen.

Wie haben wir hier zu einem besseren Design geführt? Nehmen wir an, Sie haben es einfach gehalten und eine Blasensorte implementiert. Es funktioniert, aber wenn Sie in die Produktion gehen und eine Million Objekte bearbeiten müssen, ist es viel zu langsam. Alles, was Sie tun müssen, ist, neue Traversal-Funktionen zu schreiben und diese einzutauschen. Sie müssen sich nicht mit der Komplexität der Behandlung der anderen drei Probleme befassen.

Sie werden feststellen, dass dies der Unterschied zwischen Unit Testing und TDD ist. Der Unit-Tester sagt, dass dies Ihre Tests zerbrechlich gemacht hat. Wenn Sie einfache Ein- und Ausgaben getestet hätten, müssten Sie jetzt keine weiteren Tests für Ihre neue Funktionalität schreiben. Der TDDer wird sagen, dass ich Bedenken angemessen getrennt habe, so dass jede Klasse, die ich habe, eine Sache und eine Sache gut macht.


1

Es ist nicht möglich, jede Permutation einer Berechnung mit vielen Variablen zu testen. Aber das ist nichts Neues, es war schon immer wahr für jedes Programm, das über die Komplexität des Spielzeugs hinausgeht. Der Zweck von Tests besteht darin, die Eigenschaft der Berechnung zu überprüfen . Zum Beispiel ist das Sortieren einer Liste mit 1000 Nummern etwas mühsam, aber jede einzelne Lösung kann sehr einfach überprüft werden. Jetzt, obwohl es 1000 gibt! Mögliche (Klassen von) Eingaben für dieses Programm und Sie können nicht alle testen. Es ist völlig ausreichend, nur 1000 Eingaben zufällig zu generieren und zu überprüfen, ob die Ausgabe tatsächlich sortiert ist. Warum? Weil es fast unmöglich ist, ein Programm zu schreiben, das 1000 zufällig erzeugte Vektoren zuverlässig sortiert, ohne im Allgemeinen auch korrekt zu sein (es sei denn, Sie manipulieren es absichtlich, um bestimmte magische Eingaben zu manipulieren ...)

Im Allgemeinen sind die Dinge etwas komplizierter. Es gibt wirklich haben Fehler gewesen , wo ein Mailer keine E - Mails an Benutzer liefern würden , wenn sie ein ‚f‘ in ihren Benutzernamen und den Tag der Woche haben Freitag ist. Aber ich halte es für vergeudete Mühe, eine solche Verrücktheit vorherzusehen. Ihre Testsuite sollte Ihnen die Gewissheit geben, dass das System die von Ihnen erwarteten Eingaben ausführt. Wenn es in bestimmten Fällen funky ist, werden Sie es früh genug bemerken, nachdem Sie den ersten funky Fall ausprobiert haben. Dann können Sie einen Test speziell für diesen Fall schreiben (der normalerweise auch eine ganze Klasse ähnlicher Fälle abdeckt).


Wenn Sie 1000 Eingänge zufällig generieren, wie testen Sie dann die Ausgänge? Sicherlich wird ein solcher Test eine Logik beinhalten, die an sich nicht getestet wird. Also testest du den Test? Wie? Der Punkt ist, dass Sie die Logik mit Statusübergängen testen sollten - bei Eingabe X sollte die Ausgabe Y sein. Ein Test, der Logik beinhaltet, ist fehleranfällig, genauso wie die Logik, die er testet. Wenn Sie ein Argument mit einem anderen Argument begründen, befinden Sie sich logischerweise auf einem skeptischen Rückschritt - Sie müssen einige Aussagen treffen. Diese Aussagen sind Ihre Tests.
Izhaki

0

Nehmen Sie die Randfälle und einige zufällige Eingabe.

So nehmen Sie das Sortierbeispiel:

  • Sortieren Sie ein paar zufällige Listen
  • Nimm eine Liste, die bereits sortiert ist
  • Nehmen Sie eine Liste in umgekehrter Reihenfolge
  • Nehmen Sie eine Liste, die fast sortiert ist

Wenn dies schnell funktioniert, können Sie sicher sein, dass es für alle Eingaben funktioniert.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.