Come fare TDD per qualcosa con molte permutazioni?


15

Quando si crea un sistema come un'intelligenza artificiale, che può prendere molti percorsi diversi molto rapidamente, o in realtà qualsiasi algoritmo che ha diversi input diversi, il set di risultati possibile può contenere un gran numero di permutazioni.

Quale approccio si dovrebbe adottare per utilizzare TDD quando si crea un sistema che genera molte, molte diverse permutazioni di risultati?


1
La bontà complessiva del sistema di intelligenza artificiale viene solitamente misurata dal test di precisione con un set di input di riferimento. Questo test è approssimativamente alla pari con i "test di integrazione". Come altri hanno già detto, è più simile alla "ricerca di algoritmi basata sui test" piuttosto che alla " progettazione basata sui test ".
rwong,

Definisci cosa intendi con "AI". È un campo di studio più di ogni particolare tipo di programma. Per una certa implementazione dell'IA, in genere non è possibile verificare alcuni tipi di cose (ad esempio: comportamento emergente) tramite TDD.
Steven Evers,

@SnOrfus Intendo nel senso più generale, rudimentale, una macchina decisionale.
Nicole,

Risposte:


7

Adottare un approccio più pratico alla risposta di pdr . TDD è tutto sulla progettazione del software piuttosto che sui test. Usa i test unitari per verificare il tuo lavoro mentre procedi.

Quindi a livello di test unitario è necessario progettare le unità in modo che possano essere testate in modo completamente deterministico. Puoi farlo prendendo tutto ciò che rende l'unità non deterministica (come un generatore di numeri casuali) e lo allontana. Supponiamo di avere un esempio ingenuo di un metodo che decide se una mossa è buona o no:

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

Questo metodo è molto difficile da testare e l'unica cosa che puoi davvero verificare nei test unitari sono i suoi limiti ... ma ciò richiede molti tentativi per arrivare ai limiti. Quindi, invece, astraggiamo la parte randomizzata creando un'interfaccia e una classe concreta che avvolge la funzionalità:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

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

}

La Deciderclasse ora deve usare la classe concreta attraverso la sua astrazione, cioè l'interfaccia. Questo modo di fare le cose si chiama iniezione di dipendenza (l'esempio che segue è un esempio di iniezione del costruttore, ma puoi farlo anche con un setter):

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

Potresti chiederti perché questo "code bloat" è necessario. Bene, per cominciare, ora puoi deridere il comportamento della parte casuale dell'algoritmo perché Deciderora ha una dipendenza che segue il IRandom"contratto". Puoi usare un framework beffardo per questo, ma questo esempio è abbastanza semplice da codificarti:

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

}

La parte migliore è che ciò può sostituire completamente l'implementazione concreta "effettiva". Il codice diventa facile da testare in questo modo:

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

Spero che questo ti dia idee su come progettare la tua applicazione in modo che le permutazioni possano essere forzate in modo da poter testare tutti i casi limite e quant'altro.


3

Il TDD rigoroso tende a rompersi un po 'per i sistemi più complessi, ma questo non importa troppo in termini pratici - una volta che si è oltre la capacità di isolare i singoli input, basta selezionare alcuni casi di test che forniscono una copertura ragionevole e utilizzarli.

Ciò richiede una certa conoscenza di ciò che l'implementazione dovrà fare bene, ma questo è più di una preoccupazione teorica - è molto improbabile che tu stia costruendo un'intelligenza artificiale che è stata specificata in dettaglio da utenti non tecnici. È nella stessa categoria del superamento dei test mediante hardcoding ai casi di test: ufficialmente il test è la specifica e l'implementazione è sia corretta che la soluzione più rapida possibile, ma in realtà non succede mai.


2

TDD non riguarda i test, riguarda il design.

Lungi dal cadere a pezzi con la complessità, eccelle in queste circostanze. Ti spingerà a considerare il problema più grande in pezzi più piccoli, che porterà a un design migliore.

Non cercare di provare ogni permutazione del tuo algoritmo. Crea test dopo test, scrivi il codice più semplice per far funzionare il test, fino a quando non avrai coperto le tue basi. Dovresti vedere cosa intendo per risolvere il problema perché sarai incoraggiato a falsificare parti del problema durante il test di altre parti, per evitare di dover scrivere 10 miliardi di test per 10 miliardi di permutazioni.

Modifica: volevo aggiungere un esempio, ma non avevo tempo prima.

Consideriamo un algoritmo di ordinamento sul posto. Potremmo andare avanti e scrivere test che coprano l'estremità superiore dell'array, l'estremità inferiore dell'array e ogni sorta di strane combinazioni nel mezzo. Per ognuno, dovremmo costruire un array completo di qualche tipo di oggetto. Ciò richiederebbe tempo.

Oppure potremmo affrontare il problema in quattro parti:

  1. Attraversa l'array.
  2. Confronta gli articoli selezionati.
  3. Cambia elementi.
  4. Coordinare i tre precedenti.

La prima è l'unica parte complicata del problema, ma estraendolo dal resto, lo hai reso molto, molto più semplice.

Il secondo è quasi certamente gestito dall'oggetto stesso, almeno facoltativamente, in molti framework di tipo statico ci sarà un'interfaccia per mostrare se tale funzionalità è implementata. Quindi non è necessario testarlo.

Il terzo è incredibilmente facile da testare.

Il quarto gestisce solo due puntatori, chiede alla classe di attraversamento di spostare i puntatori in giro, chiede un confronto e in base al risultato di tale confronto, chiede che gli elementi vengano scambiati. Se hai risolto i primi tre problemi, puoi provarlo molto facilmente.

Come abbiamo portato a un design migliore qui? Supponiamo che tu l'abbia reso semplice e abbia implementato una sorta di bolla. Funziona ma, quando vai in produzione e deve gestire un milione di oggetti, è troppo lento. Tutto quello che devi fare è scrivere nuove funzionalità di attraversamento e scambiarle. Non devi fare i conti con la complessità della gestione degli altri tre problemi.

Questa, troverete, è la differenza tra test unitari e TDD. Il tester dell'unità dirà che ciò ha reso fragili i test, che se avessi testato input e output semplici, non dovresti ora scrivere altri test per la tua nuova funzionalità. Il TDDer dirà che ho separato le preoccupazioni in modo adeguato in modo che ogni classe che ho faccia una cosa e una cosa bene.


1

Non è possibile testare ogni permutazione di un calcolo con molte variabili. Ma non è una novità, è sempre stato vero per qualsiasi programma al di sopra della complessità dei giocattoli. Il punto di test è verificare la proprietà del calcolo. Ad esempio, ordinare un elenco con 1000 numeri richiede un certo sforzo, ma ogni singola soluzione può essere verificata molto facilmente. Adesso, sebbene ce ne siano 1000! possibili (classi di) input per quel programma e non è possibile testarli tutti, è completamente sufficiente generare 1000 input in modo casuale e verificare che l'output sia effettivamente ordinato. Perché? Perché è quasi impossibile scrivere un programma che ordina in modo affidabile 1000 vettori generati casualmente senza essere anche corretti in generale (a meno che non lo si decida deliberatamente per manipolare determinati input magici ...)

Ora, in generale, le cose sono un po 'più complicate. Ci sono stati davvero dei bug in cui un mailer non consegnava e-mail agli utenti se avevano una "f" nel loro nome utente e il giorno della settimana è venerdì. Ma considero uno sforzo sprecato nel tentativo di anticipare una tale stranezza. La tua suite di test dovrebbe fornirti la certezza costante che il sistema fa quello che ti aspetti sugli input che ti aspetti. Se fa cose funky in alcuni casi funky, noterai abbastanza presto dopo aver provato il primo caso funky, e quindi puoi scrivere un test specificamente contro quel caso (che di solito coprirà anche un'intera classe di casi simili).


Dato che si generano 1000 input in modo casuale, come si testano gli output? Sicuramente tale test coinvolgerà un po 'di logica, che di per sé non è testata. Quindi testare il test? Come? Il punto è che dovresti testare la logica usando le transizioni di stato - dato l'ingresso X l'uscita dovrebbe essere Y. Un test che coinvolge la logica è soggetto a errori tanto quanto la logica che testa. In termini logici, giustificare un argomento con un altro argomento ti mette sulla scettica via del regresso - devi fare alcune affermazioni. Queste affermazioni sono i tuoi test.
Izhaki,

0

Prendi i casi limite più alcuni input casuali.

Per prendere l'esempio di ordinamento:

  • Ordina alcuni elenchi casuali
  • Prendi un elenco che è già ordinato
  • Prendi un elenco che è in ordine inverso
  • Prendi un elenco che è quasi ordinato

Se funziona velocemente per questi, puoi essere abbastanza sicuro che funzionerà per tutti gli input.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.