2012-06-13 8 views
41

Sembra che ci siano due approcci totalmente diversi al test, e vorrei citarli entrambi.Test: come concentrarsi sul comportamento anziché sull'implementazione senza perdere velocità?

Il fatto è che queste opinioni sono state pronunciate 5 anni fa (2007), e sono interessato, cosa è cambiato da allora e da che parte dovrei andare.

Brandon Keepers:

La teoria è che i test dovrebbero essere agnostica del attuazione. Questo porta a test meno fragili e in realtà prova l'esito (o il comportamento).

Con RSpec, mi sento come l'approccio comune di scherno completamente tuoi modelli per testare i controller finisce per costringere a guardare troppo nell'attuazione del controller.

Questo di per sé non è un problema, ma il problema è che anche esso identifica lo molto nel controller per dettare come viene utilizzato il modello. Perché è importante il se il mio controller chiama Thing.new? Cosa succede se il mio controller decide di di prendere Thing.create! e la via di salvataggio? Cosa succede se il mio modello ha un metodo di inizializzazione speciale , come Thing.build_with_foo? Le mie specifiche per il comportamento di non dovrebbero fallire se cambio l'implementazione.

Questo problema peggiora ulteriormente quando si hanno risorse nidificate e sono creazione di più modelli per controller. Alcuni dei miei metodi di installazione terminano con lo fino a 15 o più righe e MOLTO fragile.

l'intenzione di RSpec è quello di isolare completamente la logica di controllo da tuoi modelli, che suona bene in teoria, ma quasi corre contro il grano per uno stack integrato come Rails. Soprattutto se si pratica lo disciplina del controller magro/grasso, la quantità di logica nel controller diventa molto piccola e l'installazione diventa enorme.

Che cosa significa fare un aspirante BDD? Facendo un passo indietro, il comportamento che I vuole veramente testare non è che il mio controller chiama Thing.new, ma che dati i parametri X, crea una nuova cosa e reindirizza ad esso.

David Chelimsky:

E 'tutta una questione di compromessi.

Il fatto che AR scelga l'ereditarietà piuttosto che la delega ci mette in un bind di test - dobbiamo essere accoppiati al database O dobbiamo essere più intimi con l'implementazione. Accettiamo questa scelta di progettazione perché sfruttiamo i vantaggi espressivi e DRY-ness.

In lotta con il dilemma, ho scelto test più rapidi al costo di leggermente più fragile. Stai scegliendo test meno fragili al costo di di quelli che funzionano leggermente più lentamente. È un compromesso in entrambi i casi.

In pratica, ho eseguito il test centinaia, se non migliaia, di volte al giorno (io uso autotest e adottare misure molto granulari) e mi cambiano se io uso “nuovi” o “Crea” quasi mai. Anche a causa di passaggi granulari, i nuovi modelli che appaiono sono piuttosto volatili all'inizio. L'approccio valid_thing_attrs minimizza il dolore da questo un po ', ma significa comunque che ogni nuovo campo obbligatorio significa che devo cambiare valid_thing_attrs.

Ma se il tuo approccio funziona in pratica per te, allora va bene! Nel caso , ti consiglio vivamente di pubblicare un plug-in con i generatori che producono gli esempi nel modo che preferisci. Sono sicuro che un sacco di persone che beneficeranno di questo persone.

Ryan Bates:

Per curiosità, come spesso si usano dei mock nei test/spec? Forse sto facendo qualcosa di sbagliato, ma lo trovo severamente limitato a . Dal passaggio a rSpec più di un mese fa, ho fatto ciò che raccomandano nei documenti in cui il controller e i livelli di vista non colpiscono affatto il database ei modelli sono completamente derisi out. Questo ti dà un buon aumento di velocità e rende alcune cose più facili, ma sto trovando i contro di fare questo superano di gran lunga i professionisti. Dal usando i mock, le mie specifiche sono diventate un incubo di manutenzione. Le specifiche hanno lo scopo di testare il comportamento, non l'implementazione. Non mi interessa se è stato chiamato un metodo, voglio solo assicurarmi che l'uscita risultante sia corretta. Poiché il mocking rende le specifiche schizzinose sull'implementazione dello , rende semplici i refactoring (che non cambiano il comportamento dello ) senza dover tornare costantemente indietro e "correggere" le specifiche. Sono molto supponente su cosa spec/test dovrebbero coprire . Un test dovrebbe interrompersi solo quando l'app si interrompe. Questo è uno dei motivi per cui faccio fatica a testare il livello vista perché lo trovo troppo rigido. Spesso si verifica un'interruzione dei test senza che l'app si interrompa quando modifica le piccole cose nella vista. Sto riscontrando lo stesso problema con le mezze . In cima a tutto questo, ho appena realizzato oggi che il tintinnio/stubing un metodo di classe (a volte) si aggira tra le specifiche. Le specifiche dovrebbero essere autonomo e non influenzato da altre specifiche. Questo rompe la regola e porta a bug insidiosi. Che cosa ho imparato da tutto questo? Be attento dove si usa il mocking. Lo stubing non è così male, ma ha ancora alcuni degli stessi problemi.

Ho impiegato le ultime ore e ho rimosso quasi tutti i mock dalle mie specifiche. Ho anche unito il controller e ho visto le specifiche in uno usando "integrated_views" nelle specifiche del controller. Sto anche caricando tutti i dispositivi per ogni specifica del controller, quindi ci sono alcuni dati di test per riempire le viste con . Il risultato finale? Le mie specifiche sono più brevi, più semplici, più consistenti, meno rigide, e testano l'intero stack insieme (modello, vista, controller) in modo che nessun bug possa scivolare attraverso le fessure. Sono non dicendo che questo è il modo "giusto" per tutti. Se il tuo progetto richiede un caso specifico molto rigido, potrebbe non essere adatto a te, ma nel mio caso questo è un mondo migliore di quello che avevo prima di usare i mock.Sono ancora pensare che lo stub è una buona soluzione in alcuni punti, quindi sto ancora facendo .

+0

È una buona domanda; Ho visto un sacco di test di unità fragili a causa di tutte le beffa che sta succedendo.I test delle unità JavaScript possono essere peggiori. –

+0

Uomo, mi piacciono tutti questi pensieri! Il problema più grande qui è probabilmente Rails. In GOOS, dicono che non dovresti simulare il codice di terze parti (che includerebbe ActiveRecord), perché non puoi apportare modifiche al design che lo riguardano. Questo è essenzialmente il problema, David sottolinea che preferirebbe usare la composizione, ma non può b/c AR scegliere l'ereditarietà (se vuole fare le cose come tradizionalmente fatto in Rails). Personalmente, ho molte idee e non so quali siano giuste. Bella domanda, però. –

risposta

16

Penso che tutte e tre le opinioni siano ancora completamente valide. Ryan e io stavamo lottando con la manutenibilità di derisione, mentre David sentiva che il compromesso di manutenzione valeva la pena per l'aumento della velocità.

Ma questi compromessi sono i sintomi di un problema più profondo, a cui David ha alluso nel 2007: ActiveRecord. Il design di ActiveRecord ti incoraggia a creare oggetti divini che fanno troppo, sanno troppo del resto del sistema e hanno troppa superficie. Questo porta a test che hanno troppo da testare, sanno troppo del resto del sistema e sono troppo lenti o fragili.

Quindi qual è la soluzione? Separare il più possibile l'applicazione dal framework. Scrivi molte piccole classi che modellano il tuo dominio e non ereditano da nulla. Ogni oggetto dovrebbe avere un'area di superficie limitata (non più di alcuni metodi) e dipendenze esplicite passate attraverso il costruttore.

Con questo approccio, ho solo scritto due tipi di test: test di unità isolate e test di sistema full-stack. Nei test di isolamento, faccio schifo o mozzo tutto ciò che non è l'oggetto in prova. Questi test sono incredibilmente veloci e spesso non richiedono nemmeno il caricamento dell'intero ambiente Rails. I test di stack completo esercitano l'intero sistema. Sono dolorosamente lenti e danno un feedback inutile quando falliscono. Scrivo solo il necessario, ma abbastanza da darmi la certezza che tutti i miei oggetti ben collaudati si integrano bene.

Sfortunatamente, non posso indicarvi un progetto di esempio che funzioni bene (ancora). Ne parlo un po 'nella mia presentazione su Why Our Code Smells, guarda la presentazione di Corey Haines su Fast Rails Tests e consiglio vivamente di leggere Growing Object Oriented Software Guided by Tests.

+3

Continuo a ritenere che il compromesso valga la pena per l'aumento della velocità delle specifiche del controller, perché stiamo stubing/beffando un livello diverso. Anche se 'new' e' create' provengono da ActiveRecord, se sono chiamati dai controller, sono effettivamente API pubbliche sul modello _your_. Faccio _non_, tuttavia, penso che valga il compromesso delle specifiche del modello perché devi eliminare dettagli di livello inferiore di ActiveRecord. –

+0

Sarei generalmente d'accordo. Anche se ultimamente raramente scrivo le specifiche del controller perché di solito sono solo un paio di righe di codice. Se ottengono più tempo di quello, allora tiro la logica in un altro oggetto che può essere testato. – bkeepers

9

Grazie per aver compilato le citazioni del 2007. È divertente guardare indietro.

Il mio attuale metodo di test è coperto in this RailsCasts episode di cui sono stato abbastanza soddisfatto. In sintesi, ho due livelli di test.

  • alto livello: io uso richiesta specifiche in RSpec, Capybara, e videoregistratore. I test possono essere contrassegnati per eseguire JavaScript, se necessario. Qui viene evitato il mocking perché l'obiettivo è testare l'intero stack. Ogni azione del controller viene testata almeno una volta, forse un paio di volte.

  • Livello basso: Qui è dove viene testata tutta la logica complessa, in primo luogo modelli e helper. Evito anche di deridere qui. I test colpiscono il database o gli oggetti circostanti quando necessario.

Avviso non ci sono controller o specifiche di visualizzazione. Ritengo che questi siano adeguatamente coperti nelle specifiche richieste.

Dato che c'è un piccolo scherzo, come faccio a mantenere i test veloci? Ecco alcuni suggerimenti.

  • Evitare logiche di diramazione eccessive nei test di livello elevato. Qualsiasi logica complessa dovrebbe essere spostata al livello inferiore.

  • Durante la generazione di record (ad esempio con Factory Girl), utilizzare prima build e passare a create se necessario.

  • Utilizzare Guard con Spork per saltare il tempo di avvio di Rails. I test rilevanti vengono spesso eseguiti in pochi secondi dopo il salvataggio del file. Utilizzare un tag :focus in RSpec per limitare i test eseguiti quando si lavora su un'area specifica. Se si tratta di una suite di test di grandi dimensioni, impostare all_after_pass: false, all_on_start: false nel Guardfile per eseguirli tutti solo quando necessario.

  • Uso più asserzioni per test. L'esecuzione dello stesso codice di installazione per ogni asserzione aumenterà notevolmente il tempo di test. RSpec stamperà la linea che ha fallito, quindi è facile individuarlo.

Trovo che il beffeggiamento aggiunga fragilità ai test ed è per questo che lo evito. È vero, può essere un grande aiuto per il design OO, ma nella struttura di un'app Rails questo non sembra altrettanto efficace. Invece mi affido molto al refactoring e lascia che il codice stesso mi dica come dovrebbe andare il design.

Questo approccio funziona meglio su applicazioni Rails di piccole e medie dimensioni senza logica di dominio complessa e complessa.

7

Grandi domande e grande discussione. @ryanb e @bkeepers riferiscono che scrivono solo due tipi di test. Prendo un approccio simile, ma ho un terzo tipo di test:

  • Test di unità: test isolati, in genere, ma non sempre, contro semplici oggetti rubino. I miei test unitari non riguardano il DB, le chiamate API di terze parti o altre cose esterne.
  • Test di integrazione: questi sono ancora focalizzati sul test di una classe; le differenze sono che integrano quella classe con le cose esterne che evito nei miei test unitari. I miei modelli avranno spesso sia test unitari che test di integrazione, in cui l'unità mette a fuoco la logica pura che può essere testata senza coinvolgere il DB, e i test di integrazione coinvolgeranno il DB. Inoltre, tendo a testare wrapper API di terze parti con test di integrazione, utilizzando VCR per mantenere i test rapidi e deterministici, ma lasciare che le build CI realizzino le richieste HTTP reali (per rilevare eventuali modifiche API).
  • Test di accettazione: test end-to-end, per un'intera funzione. Non si tratta solo di test dell'interfaccia utente via capibara; Faccio lo stesso con le mie gemme, che potrebbero non avere un'interfaccia utente HTML. In questi casi, questo esercita qualsiasi cosa la gemma faccia per end-to-end. Tendo anche a usare VCR in questi test (se fanno richieste HTTP esterne), e come nei miei test di integrazione, la mia build CI è configurata per rendere reali le richieste HTTP.

Per quanto riguarda il mocking, non ho un approccio "taglia unica". In passato ho sicuramente superato il limite, ma trovo comunque che sia una tecnica molto utile, specialmente quando si utilizza qualcosa come rspec-fire. In generale, faccio ridere i collaboratori che giocano liberamente dei ruoli (in particolare se li possiedo e sono oggetti di servizio) e cerco di evitarlo nella maggior parte degli altri casi.

Probabilmente il più grande cambiamento ai miei test nell'ultimo anno è stato ispirato da DAS: mentre avevo un spec_helper.rb che carica l'intero ambiente, ora carico solo la classe sotto test (e tutte le dipendenze). Oltre alla migliore velocità di test (che fa una grande differenza!) Mi aiuta a capire quando la mia classe sotto test sta facendo troppe dipendenze.

Problemi correlati