Ruby Page Objects för Capybara Connoisseurs

Vad du ska skapa

Vad är sidobjekt?

Jag ger dig den korta kanten först. Det är ett designmönster som inkapslar markup- och sidinteraktioner, speciellt för att refaktorera dina egenskaper. Det är en kombination av två mycket vanliga refactoringtekniker: Extrahera klassen och Extraktmetod-som inte behöver hända samtidigt eftersom du gradvis kan bygga upp för att extrahera en hel klass via ett nytt sidobjekt.

Med denna teknik kan du skriva egenskaper på hög nivå som är mycket uttrycksfulla och torka. På ett sätt är de acceptanstest med applikationsspråk. Du kanske frågar, är inte specifikationer skrivna med Capybara redan på hög nivå och uttrycksfull? Visst, för utvecklare som skriver kod dagligen, läser Capybara specs bara bra. Torkar de ut ur lådan? Inte riktigt faktiskt, absolut inte!

"ruby-funktion" M skapar ett uppdrag "gör scenario" framgångsrikt "gör sign_in_s" [email protected] "

besök missions_path click_on 'Skapa uppdrag' fill_in 'Mission Name' med: 'Project Moonraker' click_button 'Submit' förvänta (sida) .to have_css 'li.mission-name', text: 'Project Moonraker' endänden "

"Ruby-funktionen" M markerar uppdrag som fullständigt "gör scenariot" framgångsrikt "gör sign_in_s" [email protected] "

besök missions_path click_on 'Skapa uppdrag' fill_in 'Mission Name' med: 'Octopussy' click_button 'Skicka' inom 'li: contains (' Octopussy ') "do click_on" Uppdrag slutfört "slutfört (sida) .to have_css' ul. uppdrag li.mission-name.completed ', text:' Octopussy 'slutet "

När du tittar på dessa exempel på funktionsspecifikationer, där ser du möjligheter att göra det bättre, och hur kan du extrahera information för att undvika dubbelarbete? Också är denna hög nivå tillräcklig för enkel modellering av användarberättelser och för icke-tekniska intressenter att förstå?

I mitt sinne finns det ett par sätt att förbättra detta och göra alla lyckliga utvecklare som kan undvika att fiska med detaljerna i att interagera med DOM när de tillämpar OOP, och andra icke-kodande lagmedlemmar har inga problem att hoppa mellan användarberättelser och dessa test. Den sista punkten är bra att ha, men de viktigaste fördelarna kommer oftast från att göra dina DOM-interaktiva specifikationer mer robusta.

Inkapsling är nyckelbegreppet med sidobjekt. När du skriver dina funktionsspecifikationer kommer du att dra nytta av en strategi för att extrahera beteendet som körs genom ett testflöde. För kvalitetskod vill du fånga interaktionerna med vissa uppsättningar av element på dina sidor, särskilt om du snubblar på upprepande mönster. När din ansökan växer vill du ha / behöver ett tillvägagångssätt som undviker att sprida den logiken över dina specifikationer.

"Tja, är det inte så överkilligt? Capybara läser bara bra! "Säger du?

Fråga dig själv: Varför skulle du inte ha alla HTML-implementeringsdetaljer på ett ställe, medan du har mer stabila test? Varför ska inte gränsöverskridande analyser ha samma kvalitet som test för applikationskod? Vill du verkligen stanna där?

På grund av vardagliga förändringar är din Capybara-kod sårbar vid spridning överallt - det introducerar möjliga brytpunkter. Låt oss säga en designer vill ändra texten på en knapp. Inte stor, eller hur? Men vill du anpassa dig till den förändringen i ett centralt omslag för det här elementet i dina specifikationer, eller föredrar du att göra det överallt? jag trodde det!

Det finns många refaktoreringar möjliga för dina egenskaper, men Sidobjekt erbjuder de renaste abstraktionerna för inkapsling av användarvänligt beteende för sidor eller mer komplicerade flöden. Du behöver inte simulera hela sidan / sidorna, men fokusera på de nödvändiga bitarna som är nödvändiga för användarens flöden. Inget behov av att överdriva det!

Acceptanstest / Funktionsspecifikationer

Innan vi fortsätter till hjärtat av frågan, vill jag ta ett steg tillbaka för nybörjare till hela testverksamheten och rensa upp lite av det lingo som är viktigt i detta sammanhang. Människor som är mer bekanta med TDD kommer inte att sakna mycket om de hoppar över.

Vad pratar vi om här? Godkännandetestning kommer vanligen i ett senare skede av projekt för att utvärdera om du har byggt upp något av värde för dina användare, produktägare eller annan intressent. Dessa tester drivs vanligtvis av kunder eller dina användare. Det är en kontroll om kraven uppfylls eller inte. Det finns något som en pyramid för alla typer av testlager, och acceptanstest är nära toppen. Eftersom denna process ofta innehåller icke-tekniska personer, är ett språk på hög nivå för att skriva dessa tester en värdefull tillgång för att kommunicera fram och tillbaka.

Funktionsspecifikationer är å andra sidan lite lägre i testkedjan. Mycket mer hög nivå än enhetstester, som fokuserar på de tekniska detaljerna och affärslogiken i dina modeller, har egenskaper som beskriver flöden på och mellan dina sidor.

Verktyg som Capybara hjälper dig att undvika att göra det manuellt, vilket innebär att du sällan måste öppna din webbläsare för att testa saker manuellt. Med dessa typer av tester gillar vi att automatisera dessa uppgifter så mycket som möjligt och test-driva interaktionen genom webbläsaren medan du skriver påståenden mot sidor. Förresten, du använder inte skaffa sig, sätta, posta eller radera som du gör med begäran specifikationer.

Funktionsspecifikationer ligner mycket på acceptanstest. Ibland känns skillnaderna för oskärpa att verkligen bryr sig om terminologin. Du skriver test som utövar hela applikationen, vilket ofta innebär ett flertal steg med användaråtgärder. Dessa interaktionstest visar om dina komponenter fungerar i harmoni när de sätts ihop.

I Ruby land är de huvudpersonen när vi arbetar med sidobjekt. Funktionsspecifikationerna är redan mycket uttrycksfulla, men de kan optimeras och rengöras genom att extrahera deras data, beteende och uppmärkning i en separat klass eller klasser.

Jag hoppas att att rensa upp den här suddiga terminologin hjälper dig att se att ha sidobjekt är lite som att göra test på acceptnivå medan du skriver egenskaper.

Capybara

Kanske borde vi också gå över det här väldigt snabbt. Detta bibliotek beskriver sig som ett "Godkännande test ram för webbapplikationer". Du kan simulera användarinteraktioner med dina sidor via ett mycket kraftfullt och bekvämt domänspecifikt språk. Enligt min personliga mening erbjuder RSpec, som är kopplad till Capybara, det bästa sättet att skriva dina funktionsspecifikationer för tillfället. Det låter dig besöka sidor, fylla i formulär, klicka på länkar och knappar och leta efter markering på dina sidor, och du kan enkelt kombinera alla typer av dessa kommandon för att interagera med dina sidor genom dina test.

Du kan i grunden undvika att öppna webbläsaren själv för att testa dessa saker manuellt oftast - vilket inte bara är mindre elegant men också mycket mer tidskrävande och felaktigt. Utan det här verktyget kan processen med "out-of-testing" -driva din kod från test på hög nivå ner i testen på enhetsnivå - bli mycket mer smärtsamt och eventuellt därför mer försummad.

Med andra ord börjar du skriva dessa funktionstest som baseras på dina användarberättelser, och därifrån går du ner i kaninhålet tills dina enhetstester ger den täckning dina egenskaper behöver. Därefter börjar när spelet är grönt, börjar spelet på nytt och du går tillbaka för att fortsätta med ett nytt funktionstest.

På vilket sätt?

Låt oss titta på två enkla exempel på funktionsspecifikationer som låter M skapa klassade uppdrag som då kan slutföras.

I uppräkningen har du en lista över uppdrag, och en lyckad avslutning skapar en ytterligare klass avslutadli av det särskilda uppdraget. Rätta saker, eller hur? Som ett första tillvägagångssätt började jag med små, mycket vanliga refactorings som extraherar vanligt beteende i metoder.

spec / funktioner / m_creates_a_mission_spec.rb

"rubin kräver" rails_helper "

funktionen "M skapar uppdrag" gör scenariot "framgångsrikt" gör sign_in_s '[email protected]'

create_classified_mission_named 'Project Moonraker' agent_sees_mission 'Project Moonraker' slutet 

def create_classified_mission_named (uppdragsnamn) besöker missions_path click_on "Skapa uppdrag" fill_in "Uppdragsnamn" med: uppdragsnamn click_button "Submit" slutet

def agent_sees_mission (mission_name) expect (page) .to have_css 'li.mission-name', text: uppdragsnamn slut

def sign_in_as (email) besök root_path fill_in 'Email', med: email click_button 'Skicka' slutänden "

spec / funktioner / agent_completes_a_mission_spec.rb

"rubin kräver" rails_helper "

funktionen 'M marks mission as complete' gör scenariot 'framgångsrikt' gör sign_in_as '[email protected]'

create_classified_mission_named 'Project Moonraker' mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' slutet 

def create_classified_mission_named (uppdragsnamn) besöker missions_path click_on "Skapa uppdrag" fill_in "Uppdragsnamn" med: uppdragsnamn click_button "Submit" slutet

def mark_mission_as_complete (mission_name) inom "li: contains ('# mission_name')" klick_on "Mission completed" slutet 

def agent_sees_completed_mission (mission_name) expect (page) .to have_css 'ul.missions li.mission-name.completed', text: uppdragsnamn slut

def sign_in_as (email) besök root_path fill_in 'Email', med: email click_button 'Skicka' slutänden "

Även om det finns andra sätt, naturligtvis, att hantera saker som sign_in_as, create_classified_mission_named och så vidare är det lätt att se hur snabbt dessa saker kan börja suga och att stapla upp.

UI-relaterade specifikationer får ofta inte den OO-behandling de behöver / förtjänar, tror jag. De har rykte om att ge för lite bang för pengarna, och självklart är utvecklare inte så speciellt förtjust i tider när de måste röra uppmärkningssakerna mycket. I mitt sinne gör det ännu viktigare att torka dessa specs upp och göra det roligt att hantera dem genom att kasta in ett par Ruby-klasser.

Låt oss göra ett litet magiskt trick där jag gömmer sidobjektets implementering för tillfället och bara visar dig slutresultatet som tillämpas på funktionen ovan:

spec / funktioner / m_creates_a_mission_spec.rb

"rubin kräver" rails_helper "

funktion "M skapar uppdrag" gör scenariot "framgångsrikt" gör sign_in_as "[email protected]" besök missions_path mission_page = Sidor :: Missions.new

mission_page.create_classified_mission_named 'Project Moonraker' förväntar sig (mission_page) .till har_mission_named 'Project Moonraker' endänden "

spec / funktioner / agent_completes_a_mission_spec.rb

"rubin kräver" rails_helper "

funktionen 'M marks mission as complete' gör scenariot 'framgångsrikt' gör sign_in_as '[email protected]' besök missions_path mission_page = Sidor :: Missions.new

mission_page.create_classified_mission_named 'Project Moonraker' mission_page.mark_mission_as_complete "Projekt Moonraker" förväntar sig (mission_page) .to have_completed_mission_named 'Project Moonraker' endänden "

Läser inte så illa, va? Du skapar i grunden uttrycksfulla omslagsmetoder på dina sidobjekt som låter dig hantera koncept på hög nivå, i stället för att fitta överallt med tarmarna i din markering hela tiden. Dina extraherade metoder gör den här typen av smutsigt arbete nu, och så är hageloperation inte ditt problem längre.

Sätt annorlunda, du inkapslar det mesta av den bullriga DOM-interaktiva koden. Jag måste dock säga att ibland intelligent extraherade metoder i dina egenskaper är tillräckliga och läs lite bättre eftersom du kan undvika att hantera sidobjekt-instanser. Hur som helst, låt oss ta en titt på genomförandet:

specs / support / funktioner / sidor / missions.rb

rubinmodul Sidoklassuppdrag inkluderar Capybara :: DSL

def create_classified_mission_named_name (mission_name) click_on "Skapa uppdrag" fill_in "Uppdragsnamn" med: uppdragsnamn click_button "Skicka" slut def mark_mission_as_complete (uppdragsnamn) inom "li: contains ('# mission_name') slutet def has_mission_named? (mission_name) mission_list.has_css? 'li', text: missionsnamn slut def har_completed_mission_named? (mission_name) mission_list.has_css? 'li.mission-name.completed', text: mission_name avsluta privat def mission_list find ('ul.missions') slutet slutet "

Det du ser är en vanlig gammal Ruby objekt-sidobjekt är i grunden mycket enkla klasser. Normalt skapar du inte sidobjekt med data (när det behövs, förstås kan du) och du skapar mestadels ett språk via API som en användare eller en icke-teknisk aktör på ett lag kan använda. När du tänker på namngivna metoder tycker jag att det är bra att fråga dig själv frågan: Hur skulle en användare beskriva flödet eller åtgärden?

Jag borde kanske lägga till att utan att inkludera Capybara, slutar musiken ganska snabbt.

rubin inkluderar Capybara :: DSL

Du undrar nog förmodligen hur dessa anpassade matchare jobbar:

"Ruby expect (page) .to have_mission_named 'Project Moonraker' förvänta (sida) .to have_completed_mission_named 'Project Moonraker'

def has_mission_named? (mission_name) ... slutet

def has_completed_mission_named? (mission_name) ... end "

RSpec genererar dessa anpassade matchare baserat på predikatmetoder på dina sidobjekt. RSpec konverterar dem genom att ta bort ? och ändras har till ha. Boom, matchare från grunden utan mycket fuzz! Lite magi, jag ger dig det, men den goda typen av trolldom, skulle jag säga.

Sedan vi parkerade vårt sidobjekt på specs / support / funktioner / sidor / missions.rb, du måste också se till att följande inte kommenteras i spec / rails_helper.rb.

ruby Dir [Rails.root.join ('spec / support / ** / *. rb')]. Varje | f | kräver f

Om du stöter på en NameError med en oinitialiserade konstanta sidor, du vet vad du ska göra.

Om du är nyfiken på vad som hände med sign_in_as metod, jag extraherade den till en modul på spec / support / sign_in_helper.rb och berättade RSpec att inkludera den modulen. Det här har inget att göra med Sidobjekt direkt - det är bara bättre att lagra testfunktionalitet som logga in på ett mer globalt tillgängligt sätt än via ett sidobjekt.

spec / support / sign_in_helper.rb

rubinmodul SignInHelper def sign_in_as (email) besök root_path fill_in 'Email', med: email click_button 'Skicka' slutet

Och du måste låta RSpec veta att du vill ha åtkomst till denna hjälparmodul:

spec / spec_helper.rb

"Ruby ... kräver" support / sign_in_helper "

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

Övergripande är det lätt att se att vi lyckats gömma Capybara-specifikationerna som att hitta element, klicka på länkar etc. Vi kan nu fokusera på funktionaliteten och mindre på den faktiska strukturen av markeringen som nu inkapslas i ett sidobjekt -DOM-strukturen borde vara den minsta av dina problem när du testar något som på hög nivå som funktionsspecifikationer.

Uppmärksamhet!

Installationsmaterial som fabriksdata tillhör specifikationerna och inte i sidobjekt. Också påståenden är förmodligen bättre placerade utanför dina sidobjekt för att uppnå en uppdelning av problem.

Det finns två olika perspektiv på ämnet. Förespråkare för att sätta påståenden i Sidobjekt säger att det hjälper till att undvika dubbla påståenden. Du kan tillhandahålla bättre felmeddelanden och uppnå en bättre "Tell, Do not Ask" -stil. Å andra sidan hävdar förespråkar för påståelsesfria sidobjekt att det är bättre att inte blanda ansvar. Tillhandahållande av åtkomst till siddata och assertionslogik är två separata problem och leder till uppblåstra sidobjekt när de blandas. Sidobjektets ansvar är åtkomst till sidsidan, och assertionslogiken hör till specifikationer.

Sidobjekttyper

Komponenter representerar de minsta enheterna och är mer fokuserade som ett formobjekt, till exempel.

sidor kombinera mer av dessa komponenter och är abstraktioner av en fullständig sida.

erfarenheter, Som du gissat nu spänner du över hela flödet över potentiellt många olika sidor. De är mer högnivå. De fokuserar på flödet som användaren upplever medan de interagerar med olika sidor. Ett kassaflöde som har ett par steg är ett bra exempel på att tänka på detta.

När varför?

Det är en bra idé att tillämpa det här designmönstret lite senare i ett projekts livscykel - när du har samlat lite komplexitet i dina egenskaper och när du kan identifiera repeterande mönster som DOM-strukturer, extraherade metoder eller andra gemensamheter som är konsekvent på dina sidor.

Så du bör nog inte börja skriva sidobjekt direkt. Du närmar dig dessa refactorings gradvis när komplexiteten och storleken på din applikation / test växer. Duplikationer och refactorings som behöver ett bättre hem genom Sidobjekt kommer att bli enklare att upptäcka över tiden.

Min rekommendation är att börja med att extrahera metoder i dina funktionsspecifikationer lokalt. När de slår kritisk massa ser de ut som uppenbara kandidater för vidare extraktion, och de flesta kommer troligen att passa profilen för Page Objects. Börja små, eftersom för tidig optimering lämnar otäcka bitmärken!

Slutgiltiga tankar

Sidobjekt ger dig möjlighet att skriva tydligare specifikationer som läser bättre och är överlag mycket mer uttrycksfulla eftersom de är mer höga. Dessutom erbjuder de en fin abstraktion för alla som gillar att skriva OO-kod. De gömmer DOM: s särdrag och gör det också möjligt att ha privata metoder som gör det smutsiga arbetet, medan de inte exponeras för det offentliga API: n. Utdragna metoder i dina egenskaper ger inte lika mycket lyx. API för sidobjekt behöver inte dela med sig av de nitty-gritty Capybara-detaljerna.

För alla scenarier när designredigeringar ändras, behöver dina beskrivningar av hur din app ska fungera inte behöva ändras när du använder Sidobjekt - dina funktionsspecifikationer är mer inriktade på interaktioner på användarnivå och bryr dig inte så mycket om detaljerna av DOM-implementationerna. Eftersom förändring är oundviklig blir sidobjekt kritiska när programmen växer och hjälper också förståelse när applikationens stora storlek innebär en kraftigt ökad komplexitet.