2009-03-11 8 views
6

Da quando ho iniziato a utilizzare rspec, ho riscontrato un problema con la nozione di proiettori. Le mie preoccupazioni principali sono le seguenti:È una cattiva pratica generare in modo casuale dati di test?

  1. Uso il test per rivelare un comportamento sorprendente. Non sono sempre abbastanza intelligente da elencare ogni possibile caso limite per gli esempi che sto testando. L'uso di dispositivi fissi sembra limitante perché verifica solo il mio codice con i casi molto specifici che ho immaginato. (Certo, la mia immaginazione è anche limitante rispetto a quali casi provo.)

  2. Uso il test come una forma di documentazione per il codice. Se ho valori di fixture hard-coded, è difficile rivelare cosa prova un test specifico. Per 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 
    

    Utilizzando il primo metodo dà al lettore alcuna indicazione quanto l'elemento più costoso è, solo che il suo prezzo è di 100. Tutti e tre i metodi di chiedere al lettore di prendere sul fede che l'apparecchio è il :expensive quello più costoso elencato in fixtures/items.yml. Un programmatore disattento potrebbe interrompere i test creando Item in before(:all) o inserendo un altro dispositivo in fixtures/items.yml. Se questo è 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. Per 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 faccio questo sono in realtà un po 'più pulito ho una classe che gestisce la generazione e la rimozione di tutti i modelli, ma questo codice è abbastanza chiaro per il mio esempio.) Quindi nell'esempio sopra, potrei provare come segue. Un avvertimento per la finta di cuore: il mio codice si basa pesantemente su uso di before(: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, il mio test migliore rivelano un comportamento sorprendente. Quando genero i dati in questo modo, occasionalmente mi imbatto in un caso limite in cui il mio codice non si comporta come previsto, ma che non avrei catturato se stavo usando solo fixture. 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 a volte fallisce al primo should. Vedere i fallimenti non deterministici in AutoSpec mi indurrebbe a capire che qualcosa non andava. Se stavo usando solo le fixture, potrebbe volerci molto più tempo per scoprire un simile bug.

I miei test fanno anche un lavoro leggermente migliore di dimostrare in codice quale sia il comportamento previsto. Il mio test chiarisce che ordinati è una serie di elementi 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, questa è una cattiva pratica? La mia paura dei proiettori è irrazionale? La scrittura di un metodo generate_random per ciascun modello funziona troppo? O funziona?

+0

La riga "3.times {@items 50})" non sembra corretta. –

+1

E ora, solo 58 mesi dopo, rispondo ... Non sembra giusto perché ha "< <" in esso ... ma non è correttamente scappato. – bobocopy

risposta

5

Questa è una risposta al tuo secondo punto:

(2) io uso il test come una forma di documentazione per il codice. Se ho valori di fixture hard-coded, è difficile rivelare cosa prova un test specifico.

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

Per questo motivo, molti utenti di RSpec hanno smesso di utilizzare gli apparecchi del tutto. Invece, costruisci gli oggetti necessari nell'esempio spec 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 il fine con un sacco di codice standard per la creazione di oggetti, si dovrebbe dare un'occhiata ad alcune delle molte biblioteche di fabbrica oggetto di prova, come ad esempio factory_girl, Machinist o FixtureReplacement.

+0

Il collegamento FixtureReplacement è rotto? –

+0

Un sacco di risposte eccellenti, ma questo è davvero interessante: c'è un modo migliore di fare ciò che voglio fare, ei miei dati di test non devono essere più "casuali". – bobocopy

+0

bobocopy: sembra così. Strano, penso che funzionasse ieri. È riparato ora. –

0

Un problema con casi di test generati casualmente è che convalidare la risposta dovrebbe essere calcolato dal codice e non si può essere sicuri che non ha bug :)

+0

Test e codice si verificano reciprocamente. Se il tuo test ha dei bug, lo scoprirai presto. :) –

4

Abbiamo pensato molto a questo mio recente progetto. Alla fine, abbiamo optato per due punti:

  • La ripetibilità dei casi di test è di fondamentale importanza. Se devi scrivere un test a caso, preparati a documentarlo ampiamente, perché se/quando fallisce, dovrai sapere esattamente perché.
  • Utilizzare la casualità come stampella per la copertura del codice significa che non si dispone di una buona copertura o che non si capisce abbastanza il dominio per sapere cosa costituisce un caso di test rappresentativo. Capire quale è vero e sistemarlo di conseguenza.

In breve, la casualità può spesso essere più un problema che non ne vale la pena. Valutare attentamente se lo si utilizzerà 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.

+0

Uso estesamente i dati dei test casuali. Non ho mai avuto una singola situazione in cui era più difficile di quanto valesse. I miei test casuali sono abbastanza semplici da poter sempre dire esattamente perché falliscono. Ho * avuto * test casuali rivelano ipotesi errate nel mio codice. I casi di test casuali sono un'idea molto migliore rispetto a quelli codificati e dovrebbero essere usati ovunque possibile. Non codificare mai i dati del test se puoi evitarlo, questo è un imbroglio al solitario. –

+0

Inoltre, non è necessario un generatore di numeri casuali ripetibile. Una copia del valore nel caso di test fallito funziona altrettanto bene. –

13

Sono sorpreso che nessuno in questo argomento o in quello Jason Baker linked to menzionato Monte Carlo Testing. Questa è l'unica volta in cui ho ampiamente utilizzato gli input test randomizzati. Tuttavia, era molto importante rendere il test riproducibile, avendo un seme costante per il generatore di numeri casuali per ogni caso di test.

+0

+1 per il commento riproducibile. Controllare lo stato iniziale del generatore casuale è molto importante. Se trovi un comportamento strano, hai intenzione di provare di nuovo. –

+0

un altro +1 per il riproducibile. – peterchen

+0

Terzo. Quando ho usato test randomizzati, aggiungo sempre un modo per segnalare e impostare il seme. Anche se in generale cerco di evitare di fare affidamento sulla randomizzazione ... – Jason

2

Un sacco di buone informazioni sono già state pubblicate, ma vedere anche: Fuzz Testing. La parola per strada è che Microsoft usa questo approccio su molti dei loro progetti.

+0

Sono contento che qualcuno abbia parlato di questo. Il test fuzz è estremamente utile, ma si noti che i test a campione dovrebbero essere * in aggiunta * a test ripetibili. – vasi

+0

@vasi Se "random" include pseudo-random, quindi non è in conflitto con la ripetibilità. Che ne dici di registrare il seme? –

1

La mia esperienza con i test è principalmente con programmi semplici scritti in C/Python/Java, quindi non sono sicuro che sia del tutto applicabile, ma ogni volta che ho un programma che accetta qualsiasi tipo di input dell'utente, io sempre includere un test con dati di input casuali o almeno dati di input generati dal computer in un modo imprevedibile, poiché non è mai possibile formulare ipotesi su ciò che gli utenti inseriranno. O, beh, tu puoi, ma se lo fai allora un hacker che non fa quella ipotesi potrebbe trovare un bug che hai completamente trascurato. L'input generato dalla macchina è il migliore (solo?) Modo che conosca per mantenere completamente il bias umano fuori dalle procedure di test. Ovviamente, per riprodurre un test fallito è necessario fare qualcosa come salvare l'input di test su un file o stamparlo (se si tratta di testo) prima di eseguire il test.

1

Il test a caso è una cattiva pratica a patto che non si disponga di una soluzione per il problema dell'orario , vale a dire determinare quale è l'esito previsto del software in base all'input.

Se hai risolto il problema dell'oracolo, puoi ottenere un ulteriore passaggio rispetto alla semplice generazione di input casuali. Puoi scegliere distribuzioni di input in modo tale che parti specifiche del tuo software vengano esercitate più che con semplici casuali.

Quindi passare da test a campione a test statistici.

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

Se si seleziona a e b a caso in int gamma, si esercitano Foo il 50% del tempo, Bar 25% del tempo e Foobar il 25% del tempo. È probabile che troverai più bug in Foo rispetto a Bar o Foobar.

Se si seleziona a in modo che sia negativo il 66,66% delle volte, Bar e Foobar vengono esercitati più che con la prima distribuzione.Infatti i tre rami vengono esercitati ogni 33,33% del tempo.

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

+0

Non hai bisogno di test statistici per questo - solo una relazione misurabile tra il tuo input e il tuo output. –

1

vi suggerisco di avere uno sguardo al macchinista:

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

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

Si potrebbe fare qualcosa di simile seminando il generatore di numeri casuali in modo coerente.

+0

Per utilizzare il macchinista è necessario disporre di ActiveRecord/Rails? –

+0

Credo che dipenda da ActiveRecord, ma puoi usarlo al di fuori di Rails. –

0

L'efficacia di tali test dipende in gran parte dalla qualità del generatore di numeri casuali che si utilizza e da quanto è corretto il codice che converte l'output di RNG in dati di test.

Se l'RNG non produce mai valori che fanno sì che il vostro codice entri in qualche condizione di bordo, questo caso non verrà coperto. Se il tuo codice che traduce l'output del RNG in input del codice che hai testato è difettoso, può capitare che anche con un buon generatore tu non colpisca ancora tutti i casi limite.

Come testerete per quello?

0

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) consiste nel verificare che nulla sia rotto.

Se trovi qualcosa che è rotto, devi includere quel test ogni volta da quel momento in poi, altrimenti non avrai un insieme coerente di test. Inoltre, se si esegue un test casuale che funziona, è necessario includere tale test, poiché è possibile che si possa rompere il codice in modo che il test non riesca.

In altre parole, se si dispone di un test che utilizza dati casuali generati al volo, penso che questa sia una cattiva idea. Se, tuttavia, si utilizza un insieme di dati casuali, QUINDI SI CONSEGNANO E RIUTILIZZANO, questa potrebbe essere una buona idea. Questo potrebbe assumere la forma di un set di semi per un generatore di numeri casuali.

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

Quindi, mi consiglia di utilizzare dati casuali per esplorare il vostro sistema, ma utilizzare i dati definiti nei test (dati che possono essere stati in origine generati in modo casuale)

0

L'utilizzo dei dati di test casuale è una pratica eccellente - dura 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 errate.

Consiglio vivamente di utilizzare Factory Girl e ffaker per questo. (Non utilizzare mai proiettori Rails per nulla in nessuna circostanza.)

Problemi correlati