Ruby Page Objects per gli intenditori di Capybara

Cosa starai creando

Cosa sono gli oggetti della pagina?

Ti darò il breve tono per primo. È un modello di progettazione per incapsulare markup e interazioni di pagina, in particolare per il refactoring delle specifiche della tua funzionalità. È una combinazione di due tecniche di refactoring molto comuni: Classe di estrazione e Metodo di estrazione-che non devono succedere allo stesso tempo perché puoi gradualmente accumulare fino a estrarre un'intera classe tramite un nuovo oggetto Page.

Questa tecnica consente di scrivere specifiche di funzionalità di alto livello che sono molto espressive e ASCIUTTE. In un certo senso, sono test di accettazione con il linguaggio dell'applicazione. Potresti chiedere, non sono specificate le specifiche con Capybara già di alto livello ed espressive? Certo, per gli sviluppatori che scrivono codice su base giornaliera, le specifiche di Capybara leggono bene. Sono ASCIUTTI fuori dalla scatola? Non proprio, anzi, certamente no!

"Ruby feature 'M crea una missione' do scenario 'con successo' do sign_in_as '[email protected]'

visita missions_path click_on 'Crea missione' fill_in 'Nome missione', con: 'Project Moonraker' click_button 'Submit' expect (page) .per have_css 'li.mission-name', text: 'Project Moonraker' end end "

"Ruby feature" M segna la missione come completa "do scenario" con successo "do sign_in_as '[email protected]'

visita missions_path click_on 'Crea missione' fill_in 'Nome missione', con: 'Octopussy' click_button 'Submit' all'interno di "li: contains ('Octopussy')" fai click_on 'Mission completed' end expect (pagina) .per have_css 'ul. missions li.mission-name.completed ', text:' End fine Octopussy '

Quando guardi questi esempi di specifiche delle funzionalità, dove vedi le opportunità per migliorare la lettura, e in che modo puoi estrarre le informazioni per evitare la duplicazione? Inoltre, questo è di alto livello sufficiente per semplificare la modellazione delle storie degli utenti e per le parti interessate non tecniche da comprendere?

Nella mia mente, ci sono un paio di modi per migliorare questo e rendere tutti felici - sviluppatori che possono evitare di manipolare i dettagli dell'interazione con il DOM durante l'applicazione di OOP e altri membri del team non codificanti che non hanno difficoltà a saltare tra le storie degli utenti e questi test. L'ultimo punto è sicuramente interessante, ma i vantaggi più importanti derivano principalmente dal rendere più robuste le specifiche per l'interazione con il DOM.

L'incapsulamento è il concetto chiave con Page Objects. Quando scrivi le specifiche della tua funzionalità, trarrai vantaggio da una strategia per estrarre il comportamento che sta guidando attraverso un flusso di prova. Per il codice di qualità, vuoi catturare le interazioni con insiemi particolari di elementi sulle tue pagine, specialmente se incappi in pattern ripetuti. Man mano che la tua applicazione cresce, vuoi / hai bisogno di un approccio che eviti di diffondere quella logica su tutte le tue specifiche.

"Beh, non è eccessivo? Capybara legge bene! "Dici?

Chiediti: perché non avresti tutti i dettagli di implementazione HTML in un unico posto mentre hai test più stabili? Perché i test di interazione UI non dovrebbero avere la stessa qualità dei test per il codice dell'applicazione? Vuoi veramente fermarti qui?

A causa di cambiamenti quotidiani, il tuo codice Capybara è vulnerabile quando viene diffuso ovunque e introduce possibili punti di interruzione. Diciamo che un designer vuole cambiare il testo su un pulsante. No biggie, giusto? Ma vuoi adattarti a quel cambiamento in un involucro centrale per quell'elemento nelle tue specifiche, o preferisci farlo dappertutto? Così ho pensato!

Ci sono molti rifattorici possibili per le specifiche della tua funzione, ma Page Objects offre le astrazioni più pulite per incapsulare il comportamento rivolto all'utente per pagine o flussi più complessi. Non è necessario simulare l'intera pagina o le pagine: concentrarsi sui bit essenziali necessari per i flussi degli utenti. Non c'è bisogno di strafare!

Test di accettazione / Specifiche delle funzioni

Prima di passare al nocciolo della questione, vorrei fare un passo indietro per le persone nuove per l'intera attività di test e chiarire parte del gergo che è importante in questo contesto. Le persone più familiari con TDD non mancheranno molto se salteranno.

Di cosa stiamo parlando qui? I test di accettazione di solito entrano in una fase successiva dei progetti per valutare se hai costruito qualcosa di valore per i tuoi utenti, il proprietario del prodotto o altro stakeholder. Questi test sono generalmente gestiti dai clienti o dai tuoi utenti. È una specie di controllo se i requisiti vengono soddisfatti o meno. C'è qualcosa come una piramide per tutti i tipi di livelli di test, e i test di accettazione sono quasi al top. Poiché questo processo include spesso persone non tecniche, un linguaggio di alto livello per scrivere questi test è un prezioso vantaggio per comunicare avanti e indietro.

Le caratteristiche tecniche, d'altra parte, sono un po 'più basse nella catena alimentare di test. Molto più di alto livello rispetto ai test unitari, che si concentrano sui dettagli tecnici e sulla logica di business dei tuoi modelli, le caratteristiche tecniche descrivono i flussi su e tra le tue pagine.

Strumenti come Capybara ti aiutano a evitare di farlo manualmente, il che significa che raramente devi aprire il browser per testare le cose manualmente. Con questo tipo di test, ci piace automatizzare il più possibile queste attività e testare l'interazione attraverso il browser durante la scrittura di asserzioni contro le pagine. A proposito, non lo usi ottenere, mettere, inviare o Elimina come fai con le specifiche richieste.

Le specifiche delle funzioni sono molto simili ai test di accettazione: a volte ritengo che le differenze siano troppo sfocate per preoccuparsi veramente della terminologia. Scrivi test che esercitano l'intera applicazione, che spesso comporta un flusso in più fasi delle azioni dell'utente. Questi test di interazione mostrano se i componenti funzionano in armonia quando vengono riuniti.

In Ruby land, sono i protagonisti principali quando abbiamo a che fare con Page Objects. Le specifiche delle caratteristiche stesse sono già molto espressive, ma possono essere ottimizzate e ripulite estraendo i loro dati, comportamenti e markup in una classe o classi separate.

Spero che chiarire questa terminologia sfocata ti aiuterà a vedere che avere Page Objects è un po 'come fare dei test a livello di accettazione mentre scrivi le specifiche delle caratteristiche.

Capybara

Forse dovremmo farlo anche molto velocemente. Questa libreria si descrive come un "Quadro di test di accettazione per applicazioni web". Puoi simulare le interazioni dell'utente con le tue pagine tramite un linguaggio specifico di dominio molto potente e conveniente. Secondo la mia opinione personale, RSpec in coppia con Capybara offre il modo migliore per scrivere le specifiche delle caratteristiche al momento. Ti consente di visitare pagine, compilare moduli, fare clic su link e pulsanti e cercare markup sulle tue pagine e puoi facilmente combinare tutti i tipi di questi comandi per interagire con le tue pagine attraverso i tuoi test.

In pratica, puoi evitare di aprire il browser per testare queste cose manualmente la maggior parte del tempo, che non è solo meno elegante ma anche molto più dispendioso in termini di tempo e di errori. Senza questo strumento, il processo di "fuori-dentro-test" - si guida il codice da test di alto livello ai test a livello di unità - sarebbe molto più doloroso e forse anche più trascurato.

In altre parole, inizi a scrivere questi test delle funzionalità basati sulle tue storie utente e da lì vai giù nella tana del coniglio finché i tuoi test unitari non forniranno la copertura necessaria per le specifiche della tua funzione. Dopodiché, quando i test sono ovviamente verdi, il gioco ricomincia e si ritorna indietro per continuare con un nuovo feature test.

Come?

Diamo un'occhiata a due semplici esempi di specifiche delle caratteristiche che consentono a M di creare missioni classificate che possono essere completate.

Nel markup, hai un elenco di missioni e un completamento riuscito crea una classe aggiuntiva completato sul Li di quella particolare missione. Roba semplice, giusto? Come primo approccio, ho iniziato con piccoli, molto comuni refactoring che estrapolano comportamenti comuni nei metodi.

spec / caratteristiche / m_creates_a_mission_spec.rb

"ruby richiede 'rails_helper'

funzione 'M crea missione' fai scenario 'con successo' fai sign_in_as '[email protected]'

create_classified_mission_named 'Project Moonraker' agent_sees_mission 'Fine progetto Moonraker' 

def create_classified_mission_named (mission_name) visita missions_path click_on 'Crea missione' fill_in 'Mission Name', con: mission_name click_button 'Submit' fine

def agent_sees_mission (mission_name) expect (page) .per avere_css 'li.mission-name', text: mission_name end

def sign_in_as (email) visita root_path fill_in 'Email', con: email click_button 'Submit' end end "

spec / caratteristiche / agent_completes_a_mission_spec.rb

"ruby richiede 'rails_helper'

funzione 'M segna missione come completa' fai scenario 'con successo' fai sign_in_as '[email protected]'

create_classified_mission_named 'Project Moonraker' mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Progetto Moonraker' fine 

def create_classified_mission_named (mission_name) visita missions_path click_on 'Crea missione' fill_in 'Mission Name', con: mission_name click_button 'Submit' fine

def mark_mission_as_complete (nome_azione) all'interno di "li: contains ('# mission_name')" fai clic su "Fine missione completata" 

def agent_sees_completed_mission (mission_name) expect (page) .per have_css 'ul.missions li.mission-name.completed', text: nome_azione fine

def sign_in_as (email) visita root_path fill_in 'Email', con: email click_button 'Submit' end end "

Anche se ci sono altri modi, ovviamente, per gestire cose come sign_in_as, create_classified_mission_named e così via, è facile vedere quanto velocemente queste cose possano iniziare a succhiare e accumulare.

Le specifiche relative all'interfaccia utente spesso non ricevono il trattamento OO di cui hanno bisogno / meritano, penso. Hanno la reputazione di fornire troppo poco per scommettere, e ovviamente gli sviluppatori non amano molto i tempi in cui devono toccare molto le cose di markup. Nella mia mente, ciò rende ancora più importante ASCIUGARE queste specifiche e rendere divertente affrontarle inserendo un paio di classi Ruby.

Facciamo un piccolo trucco magico in cui nascondo l'implementazione dell'oggetto Pagina per ora e ti mostro solo il risultato finale applicato alle specifiche della funzione sopra riportate:

spec / caratteristiche / m_creates_a_mission_spec.rb

"ruby richiede 'rails_helper'

funzione 'M crea missione' fai scenario 'con successo' fai firm_in_as '[email protected]' visita missions_path mission_page = Pages :: Missions.new

mission_page.create_classified_mission_named aspettato 'Project Moonraker' (mission_page) .per avere_mission_named 'Fine del progetto Moonraker' "

spec / caratteristiche / agent_completes_a_mission_spec.rb

"ruby richiede 'rails_helper'

funzione 'M segna missione come completa' fai scenario 'con successo' fai sign_in_as '[email protected]' visita missions_path mission_page = Pages :: Missions.new

mission_page.create_classified_mission_named 'Project Moonraker' mission_page.mark_mission_as_complete 'Project Moonraker' expect (mission_page) .to have_completed_mission_named 'Fine del progetto Moonraker' "

Non legge troppo male, eh? Fondamentalmente crei metodi wrapper espressivi sugli oggetti Page che ti consentono di gestire concetti di alto livello, invece di giocherellare ovunque con l'intestazione del tuo markup in ogni momento. I tuoi metodi estratti fanno questo tipo di lavoro sporco ora, e in quel modo la chirurgia a pallini non è più il tuo problema.

In altre parole, incapsula la maggior parte del codice di interazione DOM rumoroso e infestato. Devo dire, però, che a volte i metodi estratti con intelligenza nelle specifiche delle funzionalità sono sufficienti e leggono un po 'meglio poiché è possibile evitare di occuparsi delle istanze di Pagina. Ad ogni modo, diamo un'occhiata all'implementazione:

spec / supporto / caratteristiche / pagine / missions.rb

"Le missioni di classe Pagine modulo ruby ​​includono Capybara :: DSL

def create_classified_mission_named (mission_name) click_on 'Create Mission' fill_in 'Mission name', con: mission_name click_button 'Submit' end def mark_mission_as_complete (nome_azione) all'interno di "li: contains ('# mission_name')" fai click_on 'Missione completata' fine end def has_mission_named? (mission_name) mission_list.has_css? 'li', testo: nome_azione end def has_completed_mission_named? (nome_azione) mission_list.has_css? 'li.mission-name.completed', text: nome_missione end private def mission_list find ('ul.missions') end end end "

Quello che vedi è un semplice oggetto Ruby vecchio: gli oggetti Page sono essenzialmente classi molto semplici. Normalmente non istanzia Oggetti della pagina con i dati (quando il bisogno si verifica, ovviamente, puoi) e crei principalmente una lingua tramite l'API che un utente o uno stakeholder non tecnico di una squadra potrebbe utilizzare. Quando pensi di nominare i tuoi metodi, penso che sia un buon consiglio porsi la domanda: in che modo un utente descrive il flusso o l'azione intrapresa?

Forse dovrei aggiungere che senza includere Capybara, la musica si ferma abbastanza velocemente.

il rubino include Capybara :: DSL

Probabilmente ti chiedi come funzionano questi abbinamenti personalizzati:

"ruby expect (page) .to have_mission_named 'Project Moonraker' expect (page) .to have_completed_mission_named 'Progetto Moonraker'

def has_mission_named? (mission_name) ... end

def has_completed_mission_named? (mission_name) ... end "

RSpec genera questi abbinamenti personalizzati basati su metodi di predicato sugli oggetti Page. RSpec li converte rimuovendo il ? e cambiamenti ha a avere. Boom, matchers da zero senza molto sfogo! Un po 'magico, ti darò questo, ma il buon tipo di magia, direi.

Dato che abbiamo parcheggiato il nostro oggetto Page su spec / supporto / caratteristiche / pagine / missions.rb, è inoltre necessario assicurarsi che quanto segue non sia commentato in spec / rails_helper.rb.

ruby Dir [Rails.root.join ('spec / support / ** / *. rb')]. each | f | richiede f

Se ti imbatti in un NameError con un Pagine costanti non inizializzate, saprai cosa fare.

Se sei curioso di cosa è successo al sign_in_as metodo, l'ho estratto in un modulo a spec / supporto / sign_in_helper.rb e ha detto a RSpec di includere quel modulo. Questo non ha nulla a che fare con Page Objects direttamente: è più sensato archiviare funzionalità di test come registrati in un modo più accessibile a livello globale che tramite un oggetto Page.

spec / supporto / sign_in_helper.rb

modulo rubino SignInHelper def sign_in_as (email) visita root_path fill_in 'Email', con: email click_button 'Submit' end end

E devi far sapere a RSpec che vuoi accedere a questo modulo helper:

spec / spec_helper.rb

"ruby ... richiede 'supporto / sign_in_helper'

RSpec.configure do | config | config.include SignInHelper ... end "

Nel complesso, è facile vedere che siamo riusciti a nascondere gli elementi di ricerca specifici di Capybara, come fare clic sui collegamenti, ecc. Ora possiamo concentrarci sulla funzionalità e meno sulla struttura effettiva del markup, che ora è incapsulato in un oggetto Page -la struttura DOM dovrebbe essere l'ultimo dei tuoi dubbi quando provi qualcosa di alto livello come le specifiche delle caratteristiche.

Attenzione!

Le impostazioni come i dati di fabbrica appartengono alle specifiche e non agli oggetti Page. Inoltre, le asserzioni sono probabilmente posizionate meglio al di fuori degli oggetti Page per ottenere una separazione delle preoccupazioni.

Ci sono due diverse prospettive sull'argomento. I sostenitori di mettere le asserzioni in Page Objects dicono che aiuta a evitare la duplicazione delle asserzioni. È possibile fornire messaggi di errore migliori e ottenere uno stile "Tell, Do not Ask" migliore. D'altro canto, i sostenitori di oggetti Page privi di asserzioni sostengono che è meglio non mescolare le responsabilità. Fornire accesso ai dati della pagina e logica di asserzione sono due preoccupazioni separate e portare a oggetti pagina gonfiati quando mescolati. La responsabilità dell'oggetto Page è l'accesso allo stato delle pagine e la logica dell'asserzione appartiene alle specifiche.

Tipi di oggetti della pagina

componenti rappresentano le unità più piccole e sono più focalizzate, ad esempio un oggetto modulo.

pagine combinare più di questi componenti e sono astrazioni di una pagina intera.

esperienze, come hai intuito ora, attraversa l'intero flusso attraverso potenzialmente molte pagine diverse. Sono più di alto livello. Si concentrano sul flusso che l'utente sperimenta mentre interagiscono con varie pagine. Un flusso di cassa che ha un paio di passaggi è un buon esempio per pensarci.

Quando perché?

È una buona idea applicare questo modello di progettazione un po 'più tardi nel ciclo di vita di un progetto, quando hai accumulato un po' di complessità nelle specifiche delle funzioni e quando puoi identificare modelli ripetuti come strutture DOM, metodi estratti o altri elementi comuni che sono coerente sulle tue pagine.

Quindi probabilmente non dovresti iniziare a scrivere Page Objects subito. Ti avvicini a questi refactoring gradualmente quando la complessità e le dimensioni della tua applicazione / test crescono. Le duplicazioni e i refactoring che necessitano di una casa migliore attraverso Page Objects saranno più facili da individuare nel tempo.

La mia raccomandazione è di iniziare con l'estrazione dei metodi nelle specifiche delle funzionalità a livello locale. Una volta raggiunta la massa critica, sembreranno candidati ovvi per un'ulteriore estrazione, e la maggior parte di essi probabilmente si adatta al profilo di Page Objects. Inizia in piccolo, perché l'ottimizzazione prematura lascia brutti segni di morso!

Pensieri finali

Gli oggetti pagina ti offrono l'opportunità di scrivere specifiche più chiare che leggono meglio e sono nel complesso molto più espressive perché sono più di alto livello. Oltre a questo, offrono una bella astrazione per tutti coloro a cui piace scrivere il codice OO. Nascondono le specifiche del DOM e ti permettono anche di avere metodi privati ​​che fanno il lavoro sporco mentre non sono esposti all'API pubblica. I metodi estratti nelle specifiche delle funzionalità non offrono lo stesso lusso. L'API di Page Objects non ha bisogno di condividere i dettagli di Capybara nitty-gritty.

Per tutti gli scenari in cui cambiano le implementazioni della progettazione, le descrizioni di come dovrebbe funzionare l'app non devono essere modificate quando si utilizzano Page Objects: le specifiche delle funzionalità sono più focalizzate sulle interazioni a livello di utente e non si preoccupano molto delle specifiche delle implementazioni DOM. Dal momento che il cambiamento è inevitabile, gli oggetti Page diventano critici quando le applicazioni crescono e aiutano anche a capire quando la dimensione dell'applicazione significa drasticamente maggiore complessità.