Domanda

Da quando ho iniziato a usare rspec, ho avuto un problema con la nozione di dispositivi. Le mie preoccupazioni principali sono queste:

  1. Uso i test per rivelare comportamenti sorprendenti. Non sono sempre abbastanza intelligente da elencare ogni possibile caso limite per gli esempi che sto testando. L'uso di dispositivi hard-coded sembra limitativo perché mette alla prova il mio codice solo con i casi molto specifici che ho immaginato. (Certo, la mia immaginazione è anche limitante rispetto a quali casi testerò.)

  2. Uso i test come forma di documentazione per il codice. Se ho valori di fixture codificati, è difficile rivelare ciò che un particolare test sta cercando di dimostrare. Ad esempio:

    describe Item do
      describe '#most_expensive' do
        it 'should return the most expensive item' do
          Item.most_expensive.price.should == 100
          # OR
          #Item.most_expensive.price.should == Item.find(:expensive).price
          # OR
          #Item.most_expensive.id.should == Item.find(:expensive).id
        end
      end
    end
    

    L'uso del primo metodo non dà al lettore alcuna indicazione su quale sia l'articolo più costoso, solo che il suo prezzo è 100. Tutti e tre i metodi chiedono al lettore di credere che l'apparecchiatura : costoso sia il più costoso elencato in fixtures / items.yml . Un programmatore incurante potrebbe superare i test creando un Item in prima (: all) o inserendo un altro dispositivo in fixtures / items.yml . Se si tratta di un file di grandi dimensioni, potrebbe essere necessario molto tempo per capire qual è il problema.

Una cosa che ho iniziato a fare è aggiungere un metodo #generate_random a tutti i miei modelli. Questo metodo è disponibile solo quando eseguo le mie specifiche. Ad esempio:

class Item
  def self.generate_random(params={})
    Item.create(
      :name => params[:name] || String.generate_random,
      :price => params[:price] || rand(100)
    )
  end
end

(I dettagli specifici di come lo faccio sono in realtà un po 'più chiari. Ho una classe che gestisce la generazione e la pulizia di tutti i modelli, ma questo codice è abbastanza chiaro per il mio esempio.) Quindi nell'esempio sopra, I potrebbe testare come segue. Un avvertimento per la finta di cuore: il mio codice si basa fortemente sull'uso di prima (: all) :

describe Item do
  describe '#most_expensive' do
    before(:all) do
      @items = []
      3.times { @items << Item.generate_random }
      @items << Item.generate_random({:price => 50})
    end

    it 'should return the most expensive item' do
      sorted = @items.sort { |a, b| b.price <=> a.price }
      expensive = Item.most_expensive
      expensive.should be(sorted[0])
      expensive.price.should >= 50      
    end
  end
end

In questo modo, i miei test rivelano meglio comportamenti sorprendenti. Quando generi dati in questo modo, di tanto in tanto mi imbatto in un caso limite in cui il mio codice non si comporta come previsto, ma che non avrei colto se stessi usando solo i dispositivi. Ad esempio, nel caso di #most_expensive , se ho dimenticato di gestire il caso speciale in cui più articoli condividono il prezzo più costoso, il mio test occasionalmente fallirebbe al primo dovrebbe . Vedere i fallimenti non deterministici in AutoSpec mi farebbe capire che qualcosa non andava. Se usassi solo dispositivi, potrebbe impiegare molto più tempo a scoprire un simile bug.

I miei test fanno anche un lavoro leggermente migliore nel dimostrare in codice quale sia il comportamento previsto. Il mio test chiarisce che ordinato è un array di articoli ordinati in ordine decrescente per prezzo. Poiché mi aspetto che #most_expensive sia uguale al primo elemento di quell'array, è ancora più ovvio quale sia il comportamento previsto di most_expensive .

Quindi, è una cattiva pratica? La mia paura delle partite è irrazionale? Scrivere un metodo generate_random per ciascun modello funziona troppo? O funziona?

È stato utile?

Soluzione

Questa è una risposta al tuo secondo punto:

  

(2) Uso i test come forma di documentazione per il codice. Se ho valori di fixture codificati, è difficile rivelare ciò che un particolare test sta cercando di dimostrare.

Sono d'accordo. Idealmente, gli esempi di specifiche dovrebbero essere comprensibili da soli. L'uso degli infissi è problematico, perché divide le pre-condizioni dell'esempio dai risultati attesi.

Per questo motivo, molti utenti di RSpec hanno smesso di usare del tutto le apparecchiature. Costruisci invece gli oggetti necessari nell'esempio di specifica stesso.

describe Item, "#most_expensive" do
  it 'should return the most expensive item' do
    items = [
      Item.create!(:price => 100),
      Item.create!(:price => 50)
    ]

    Item.most_expensive.price.should == 100
  end
end

Se finisci con un sacco di codice boilerplate per la creazione di oggetti, dovresti dare un'occhiata ad alcune delle molte librerie di fabbrica di oggetti di test, come factory_girl , Machinist o < a href = "http://replacefixtures.rubyforge.org/" rel = "nofollow noreferrer"> FixtureReplacement .

Altri suggerimenti

Non sono sorpreso nessuno in questo argomento o in quello Jason Baker collegato a menzionato Test di Monte Carlo . Questa è l'unica volta che ho ampiamente utilizzato input di test randomizzati. Tuttavia, è stato molto importante rendere riproducibile il test, avendo un seme costante per il generatore di numeri casuali per ciascun caso di test.

Ci abbiamo pensato molto su un mio recente progetto. Alla fine, abbiamo optato per due punti:

  • La ripetibilità dei casi di test è di fondamentale importanza. Se devi scrivere un test casuale, preparati a documentarlo ampiamente, perché se / quando fallisce, dovrai sapere esattamente perché.
  • L'uso della casualità come stampella per la copertura del codice significa che non hai una buona copertura o non capisci abbastanza il dominio per sapere cosa costituisce un caso rappresentativo. Scopri cos'è vero e correggilo di conseguenza.

In breve, la casualità può spesso essere più un problema di quanto valga la pena. Valuta attentamente se lo userai correttamente prima di premere il grilletto. Alla fine abbiamo deciso che i casi di test casuali erano una cattiva idea in generale e da usare con parsimonia, se non del tutto.

Molte buone informazioni sono già state pubblicate, ma vedi anche: Fuzz Testing . Word on the street è che Microsoft utilizza questo approccio su molti dei suoi progetti.

La mia esperienza con i test è principalmente con semplici programmi scritti in C / Python / Java, quindi non sono sicuro che ciò sia del tutto applicabile, ma ogni volta che ho un programma in grado di accettare qualsiasi tipo di input dell'utente, includo sempre un test con dati di input casuali, o almeno dati di input generati dal computer in modo imprevedibile, perché non si possono mai fare ipotesi su ciò che gli utenti inseriranno. O, bene, puoi , ma se lo fai, qualche hacker che non fa questa ipotesi potrebbe trovare un bug che hai completamente ignorato. L'input generato dalla macchina è il modo migliore (solo?) Che conosco per mantenere la distorsione umana completamente fuori dalle procedure di test. Naturalmente, per riprodurre un test fallito devi fare qualcosa come salvare l'input del test in un file o stamparlo (se è testo) prima di eseguire il test.

I test casuali sono una cattiva pratica purché non si abbia una soluzione per il problema dell'oracolo , vale a dire determinare quale sia il risultato atteso del software dato il suo input.

Se hai risolto il problema dell'oracolo, puoi fare un passo in più rispetto alla semplice generazione di input casuale. Puoi scegliere le distribuzioni di input in modo tale che parti specifiche del tuo software vengano esercitate più che con un semplice casuale.

Passa quindi dai test casuali ai test statistici.

if (a > 0)
    // Do Foo
else (if b < 0)
    // Do Bar
else
    // Do Foobar

Se selezioni a e b casualmente nell'intervallo int , eserciti Foo il 50% delle volte , Bar il 25% delle volte e Foobar il 25% delle volte. È probabile che troverai più bug in Foo che in Bar o Foobar .

Se selezioni a in modo che sia negativo il 66,66% del tempo, Bar e Foobar vengono esercitati più che con la tua prima distribuzione . In effetti, le tre filiali vengono esercitate ogni 33,33% delle volte.

Naturalmente, se il risultato osservato è diverso da quello previsto, è necessario registrare tutto ciò che può essere utile per riprodurre il bug.

Suggerirei di dare un'occhiata a Machinist:

  

http://github.com/notahat/machinist/tree/master

Machinist genererà dati per te, ma è ripetibile, quindi ogni test ha gli stessi dati casuali.

Puoi fare qualcosa di simile seminando costantemente il generatore di numeri casuali.

Un problema con i casi di test generati casualmente è che la convalida della risposta deve essere calcolata dal codice e non si può essere sicuri che non abbia bug :)

Potresti anche visualizzare questo argomento: Test con best practice per input casuali .

L'efficacia di tali test dipende in gran parte dalla qualità del generatore di numeri casuali che si utilizza e dalla correttezza del codice che traduce l'output di RNG in dati di test.

Se l'RNG non produce mai valori che inducono il tuo codice a entrare in una condizione di caso limite, questo caso non sarà coperto. Se il tuo codice che traduce l'output del RNG in input del codice testato è difettoso, può accadere che anche con un buon generatore non colpisca ancora tutti i casi limite.

Come lo testerai?

Il problema con la casualità nei casi di test è che l'output è, beh, casuale.

L'idea alla base dei test (in particolare i test di regressione) è quella di verificare che nulla sia rotto.

Se trovi qualcosa che non funziona, devi includere quel test ogni volta da quel momento in poi, altrimenti non avrai un set coerente di test. Inoltre, se esegui un test casuale che funziona, devi includere quel test, perché è possibile che tu possa rompere il codice in modo che il test fallisca.

In altre parole, se hai un test che utilizza dati casuali generati al volo, penso che questa sia una cattiva idea. Se tuttavia, usi una serie di dati casuali, CHE CONSERVI E RIUTILIZI, questa potrebbe essere una buona idea. Questo potrebbe assumere la forma di un insieme di semi per un generatore di numeri casuali.

Questa memorizzazione dei dati generati consente di trovare la risposta "corretta" a questi dati.

Quindi, consiglierei di usare dati casuali per esplorare il tuo sistema, ma usare dati definiti nei tuoi test (che potrebbero essere stati originariamente generati casualmente)

L'uso di dati di test casuali è una pratica eccellente: i dati di test codificati testano solo i casi a cui hai pensato esplicitamente, mentre i dati casuali eliminano le tue ipotesi implicite che potrebbero essere sbagliate.

Consiglio vivamente di usare Factory Girl e ffaker per questo. (Non utilizzare mai gli apparecchi Rails per nessun motivo.)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top