Ruby Page Objects voor Capybara Connoisseurs

Wat je gaat creëren

Wat zijn pagina-objecten?

Ik geef je eerst de korte toon. Het is een ontwerppatroon om markup- en pagina-interacties in te kapselen, met name om je featurenspecificaties te refactiveren. Het is een combinatie van twee zeer algemene refactoringtechnieken: Klasse extraheren en Extractiemethode-die niet tegelijkertijd hoeven te gebeuren omdat je geleidelijk kunt opbouwen om een ​​hele klas te extraheren via een nieuw Page Object.

Met deze techniek kun je geavanceerde functiespecificaties schrijven die erg expressief en DROOG zijn. In zekere zin zijn het acceptatietests met de toepassingstaal. Je zou je kunnen afvragen, zijn specificaties niet geschreven met Capybara al op hoog niveau en expressief? Zeker, voor ontwikkelaars die dagelijks code schrijven, lezen de Capybara-specificaties prima. Zijn ze droog uit de doos? Niet echt, eigenlijk, zeker niet!

"Ruby-functie" M maakt een missie 'do-scenario' met succes 'do sign_in_as' [email protected] '

bezoek missions_path click_on 'Create Mission' fill_in 'Mission Name', met: 'Project Moonraker' click_button 'Submit' expect (pagina) .to have_css 'li.mission-name', tekst: 'Project Moonraker' end end "

"ruby-functie" M markeert missie als volledig 'do scenario' succesvol 'do sign_in_as' [email protected] '

bezoek missions_path click_on 'Create Mission' fill_in 'Mission Name', met: 'Octopussy' click_button 'Verzenden' binnen 'li: contains (' Octopussy ') "do click_on' Mission completed 'end verwachten (pagina) .to have_css' ul. missies li.mission-name.completed ', text:' Octopussy 'end end "

Wanneer u naar deze voorbeelden van functie-specificaties kijkt, waar ziet u dan kansen om dit beter te laten lezen en hoe kunt u informatie extraheren om dubbel werk te voorkomen? Ook is dit hoog niveau voldoende om eenvoudig gebruikersverhalen te modelleren en voor niet-technische belanghebbenden te begrijpen?

In mijn gedachten zijn er een aantal manieren om dit te verbeteren en om iedereen blij te maken - ontwikkelaars die niet hoeven te hacken met de details van de interactie met de DOM tijdens het toepassen van OOP en andere niet-coderende teamleden die geen problemen hebben om tussen gebruikersverhalen te springen. en deze tests. Het laatste punt is zeker leuk, maar de belangrijkste voordelen komen vooral van het robuuster maken van je DOM-interactieve specs.

Inkapseling is het belangrijkste concept met Page Objects. Wanneer u uw kenmerkspecificaties schrijft, profiteert u van een strategie om het gedrag dat door een teststroom rijdt, te extraheren. Voor kwaliteitscode wilt u de interacties vastleggen met bepaalde sets elementen op uw pagina's, vooral als u struikelt over zich herhalende patronen. Naarmate uw toepassing groeit, wilt u / heeft u een aanpak nodig die voorkomt dat die logica over uw specificaties wordt verspreid.

"Nou, is dat niet overdreven? Capybara leest het prima! ", Zeg je?

Stel jezelf de vraag: Waarom zou je niet alle HTML-implementatiegegevens op één plek hebben terwijl je stabielere tests hebt? Waarom zouden gebruikersinteractie-testen niet dezelfde kwaliteit hebben als tests voor toepassingscode? Wil je daar echt stoppen??

Door dagelijkse veranderingen is je Capybara-code kwetsbaar wanneer je hem overal verspreidt - het introduceert mogelijke breekpunten. Laten we zeggen dat een ontwerper de tekst op een knop wil wijzigen. Geen buistelevisie, toch? Maar wilt u zich aanpassen aan die verandering in één centrale verpakking voor dat element in uw specificaties, of doet u dat liever overal? ik dacht het al!

Er zijn veel refactorings mogelijk voor uw kenmerkspecificaties, maar pagina-objecten bieden de schoonste abstracties voor het inkapselen van gebruikersgericht gedrag voor pagina's of meer complexe stromen. U hoeft de hele pagina ('s) echter niet te simuleren - focus op de essentiële bits die nodig zijn voor gebruikersstromen. Het is niet nodig om het te overdrijven!

Acceptatietests / Feature-specificaties

Voordat we verder gaan met de kern van de zaak, wil ik graag een stap terug doen voor mensen die nieuw zijn in de hele testwereld en een deel van de lingo opruimen die belangrijk is in deze context. Mensen die bekend zijn met TDD zullen niet veel missen als ze verder gaan.

Waar hebben we het hier over? Acceptatietests komen meestal pas in een later stadium van projecten om te beoordelen of u iets van waarde hebt opgebouwd voor uw gebruikers, producteigenaar of andere belanghebbende. Deze tests worden meestal uitgevoerd door klanten of uw gebruikers. Het is een soort controle als aan de vereisten wordt voldaan of niet. Er is zoiets als een piramide voor allerlei testlagen en acceptatietests zijn bijna de top. Omdat dit proces vaak niet-technische mensen omvat, is een taal op hoog niveau voor het schrijven van deze tests een waardevol instrument om heen en weer te communiceren.

Feature specs, aan de andere kant, zijn een beetje lager in de testende voedselketen. Veel meer hoog niveau dan unit tests, die zich richten op de technische details en bedrijfslogica van uw modellen, functie specificaties beschrijven stromen op en tussen uw pagina's.

Tools zoals Capybara helpen u om dit niet handmatig te doen, wat betekent dat u zelden uw browser hoeft te openen om dingen handmatig te testen. Met dit soort tests willen we deze taken zo veel mogelijk automatiseren en de interactie testen via de browser tijdens het schrijven van beweringen tegen pagina's. Trouwens, je gebruikt het niet krijgen, leggen, post of verwijderen zoals u doet met verzoekspecificaties.

Functiespecificaties lijken erg op acceptatietests - soms vind ik de verschillen te wazig om echt om de terminologie te geven. Je schrijft tests die je hele applicatie oefenen, wat vaak een meervoudige stroom van gebruikersacties met zich meebrengt. Deze interactietests laten zien of uw componenten in harmonie werken wanneer ze bij elkaar worden gebracht.

In Robijnland zijn ze de hoofdrolspeler wanneer we te maken hebben met Page Objects. Feature-specificaties zelf zijn al erg expressief, maar ze kunnen worden geoptimaliseerd en opgeschoond door hun gegevens, gedrag en markeringen in een afzonderlijke klasse of klassen te extraheren.

Ik hoop dat het opruimen van deze wazige terminologie je zal helpen inzien dat het hebben van Page Objects een beetje lijkt op testen op acceptatieniveau tijdens het schrijven van functie-specificaties.

Capybara

Misschien moeten we dit ook heel snel bespreken. Deze bibliotheek beschrijft zichzelf als een "Acceptance test framework for web applications". U kunt gebruikersinteracties met uw pagina's simuleren via een zeer krachtige en handige domeinspecifieke taal. Naar mijn mening biedt RSpec in combinatie met Capybara de beste manier om je featurespecificaties op dit moment te schrijven. Hiermee kunt u pagina's bezoeken, formulieren invullen, op koppelingen en knoppen klikken en naar markeringen op uw pagina's zoeken, en kunt u eenvoudig allerlei soorten opdrachten combineren om met uw pagina's te werken via uw tests.

U kunt in principe voorkomen dat u de browser zelf opent om dit soort dingen handmatig te testen, wat niet alleen minder elegant is, maar ook veel meer tijdrovend en foutgevoelig. Zonder deze tool zou het proces van "outside-in-testing" - u rijdt uw code van tests op hoog niveau naar uw unit-level tests - veel pijnlijker en mogelijk daarom meer verwaarloosd worden.

Met andere woorden, u begint met het schrijven van deze functietests die zijn gebaseerd op uw gebruikersverhalen, en van daaruit gaat u door het konijnenhol totdat uw unit-tests de dekking bieden die uw featurenspecificaties nodig hebben. Daarna, wanneer je tests natuurlijk groen zijn, begint het spel opnieuw en ga je terug naar boven om door te gaan met een nieuwe functietest.

Hoe?

Laten we eens kijken naar twee eenvoudige voorbeelden van functiespecificaties waarmee M geheime missies kan maken die vervolgens kunnen worden voltooid.

In de markup heb je een lijst met missies en een succesvolle afronding creëert een extra klasse voltooid op de li van die specifieke missie. Simpele dingen, toch? Als een eerste benadering ben ik begonnen met kleine, veel voorkomende refactorings die gewoon gedrag in methoden ontlenen.

spec / kenmerken / m_creates_a_mission_spec.rb

"ruby require 'rails_helper'

feature 'M creëert missie' do scenario 'succesvol' do sign_in_as '[email protected]'

create_classified_mission_named 'Project Moonraker' agent_sees_mission 'Project Moonraker' einde 

def create_classified_mission_named (mission_name) bezoek missions_path click_on 'Create Mission' fill_in 'Mission Name', met: mission_name click_button 'Submit' end

def agent_sees_mission (mission_name) verwacht (pagina) .to have_css 'li.mission-name', text: mission_name end

def sign_in_as (email) bezoek root_path fill_in 'Email', met: email click_button 'Submit' end end "

spec / kenmerken / agent_completes_a_mission_spec.rb

"ruby require 'rails_helper'

kenmerk 'M markeert missie als volledig' do scenario 'succesvol' do sign_in_as '[email protected]'

create_classified_mission_named 'Project Moonraker' mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' einde 

def create_classified_mission_named (mission_name) bezoek missions_path click_on 'Create Mission' fill_in 'Mission Name', met: mission_name click_button 'Submit' end

def mark_mission_as_complete (mission_name) binnen "li: contains ('# mission_name')" do click_on 'Mission completed' end end 

def agent_sees_completed_mission (mission_name) verwacht (pagina) .to have_css 'ul.missions li.mission-name.completed', text: mission_name end

def sign_in_as (email) bezoek root_path fill_in 'Email', met: email click_button 'Submit' end end "

Hoewel er natuurlijk ook andere manieren zijn om met dingen als deze om te gaan sign_in_as, create_classified_mission_named en zo verder, het is gemakkelijk om te zien hoe snel deze dingen kunnen beginnen te zuigen en op te stapelen.

UI-gerelateerde specs krijgen vaak niet de OO-behandeling die ze nodig hebben / verdienen, denk ik. Ze hebben de reputatie om te weinig bang voor de bok te bieden, en natuurlijk zijn ontwikkelaars niet erg gesteld op tijden waarin ze veel van markup dingen moeten raken. In mijn gedachten maakt dat het nog belangrijker om deze specificaties te DROGEN en het leuk te maken om ermee om te gaan door een paar Ruby-lessen erin te gooien.

Laten we een kleine goocheltruc doen waarbij ik de implementatie van het paginaobject voorlopig verberg en alleen het eindresultaat laat zien dat is toegepast op de bovenstaande specificaties van het object:

spec / kenmerken / m_creates_a_mission_spec.rb

"ruby require 'rails_helper'

feature 'M creëert missie' do scenario 'successful' do sign_in_as '[email protected]' bezoek missions_path mission_page = Pagina's :: Missions.new

mission_page.create_classified_mission_named 'Project Moonraker' expect (mission_page) .to have_mission_named 'Project Moonraker' end end "

spec / kenmerken / agent_completes_a_mission_spec.rb

"ruby require 'rails_helper'

feature 'M markeert missie als compleet' do scenario 'succesvol' do sign_in_as '[email protected]' bezoek missions_path mission_page = Pagina's :: Missions.new

mission_page.create_classified_mission_named 'Project Moonraker' mission_page.mark_mission_as_complete 'Project Moonraker' verwachten (mission_page) .to have_completed_mission_named 'Project Moonraker' end end "

Leest niet slecht, huh? Je maakt in feite expressieve wrapper-methoden op je pagina-objecten waarmee je op hoog niveau kunt omgaan - in plaats van overal met de ingewanden van je opmaak overal aan te haken. Je geëxtraheerde methoden doen dit soort vuile werk nu, en op die manier is shotgun-chirurgie niet meer jouw probleem.

Anders gezegd, je kapselt de meeste van de luidruchtige, onkruid-onkruid DOM-interactiecode in. Ik moet echter wel zeggen dat soms intelligent geëxtraheerde methoden in uw featurenspecificaties voldoende zijn en een beetje beter lezen, omdat u de omgang met instanties van Pagina-objecten kunt vermijden. Hoe dan ook, laten we de implementatie eens bekijken:

specs / support / features / pages / missions.rb

"ruby module Pagina's klasse Missies omvatten Capybara :: DSL

def create_classified_mission_named (mission_name) click_on 'Create Mission' fill_in 'Mission name', met: mission_name click_button 'Submit' end def mark_mission_as_complete (mission_name) binnen "li: contains ('# mission_name')" do click_on 'Mission completed' end einde def has_mission_named? (mission_name) mission_list.has_css? 'li', tekst: mission_name end def has_completed_mission_named? (mission_name) mission_list.has_css? 'li.mission-name.completed', text: mission_name end private def mission_list find ('ul.missions') end end end "

Wat je ziet is een eenvoudig oud Ruby-object. Pagina-objecten zijn in essentie heel eenvoudige klassen. Normaal gesproken start u geen pagina-objecten met gegevens (als dat nodig is, kunt u dat natuurlijk) en maakt u meestal een taal via de API die een gebruiker of een niet-technische belanghebbende in een team kan gebruiken. Als je nadenkt over het benoemen van je methoden, denk ik dat het een goed advies is om jezelf de vraag te stellen: hoe zou een gebruiker de stroom of de genomen actie beschrijven?

Ik zou hieraan moeten toevoegen dat zonder Capybara te includeren, de muziek behoorlijk snel stopt.

robijn omvatten Capybara :: DSL

Je vraagt ​​je waarschijnlijk af hoe deze aangepaste matchers werken:

"robijn verwacht (pagina) .to have_mission_named 'Project Moonraker' verwacht (pagina) .to have_completed_mission_named 'Project Moonraker'

def has_mission_named? (mission_name) ... end

def has_completed_mission_named? (mission_name) ... end "

RSpec genereert deze aangepaste overeenkomsten op basis van predikaatmethoden op uw pagina-objecten. RSpec converteert ze door de ? en wijzigingen heeft naar hebben. Boom, matchers vanaf nul zonder veel fuzz! Een beetje magie, ik zal je dat geven, maar de goede soort tovenarij zou ik zeggen.

Sinds we ons pagina-object hebben geparkeerd specs / support / features / pages / missions.rb, je moet ook zorgen dat het volgende niet wordt becommentarieerd spec / rails_helper.rb.

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

Als je een tegenkomt NameError Met een niet-geïnitialiseerde constante pagina's, je zult weten wat je moet doen.

Als je benieuwd bent wat er met de. Is gebeurd sign_in_as methode, ik heb het in een module uitgepakt bij spec / support / sign_in_helper.rb en vertelde RSpec om die module op te nemen. Dit heeft niets te maken met Pagina-objecten - het is gewoon logischer om testfunctionaliteit op te slaan zoals aanmelden op een meer globaal toegankelijke manier dan via een Page Object.

spec / support / sign_in_helper.rb

ruby module SignInHelper def sign_in_as (email) bezoek root_path fill_in 'Email', met: email click_button 'Submit' end end

En je moet RSpec laten weten dat je toegang wilt tot deze helper-module:

spec / spec_helper.rb

"ruby ... vereisen 'support / sign_in_helper'

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

Over het algemeen is het gemakkelijk te zien dat we erin geslaagd zijn de Capybara-kenmerken te verbergen, zoals zoekelementen, klikken op links, enzovoort. We kunnen ons nu concentreren op de functionaliteit en minder op de daadwerkelijke structuur van de opmaak, die nu is ingekapseld in een pagina-object - de DOM-structuur moet het minste van uw zorgen zijn wanneer u iets test dat zo hoog is als de specificaties van functies.

Aandacht!

Het instellen van dingen zoals fabrieksgegevens hoort thuis in de specificaties en niet in pagina-objecten. Ook worden beweringen waarschijnlijk beter geplaatst buiten uw pagina-objecten om een ​​scheiding van punten van zorg te bereiken.

Er zijn twee verschillende perspectieven op het onderwerp. Pleitbezorgers voor het doen van beweringen in Page Objects zeggen dat het helpt met het vermijden van duplicatie van beweringen. U kunt betere foutmeldingen geven en een betere "Tell, Do not Ask" -stijl bereiken. Aan de andere kant beweren pleitbezorgers voor bewering-vrije pagina-objecten dat het beter is om verantwoordelijkheden niet te combineren. Het bieden van toegang tot paginagegevens en assertieve logica zijn twee afzonderlijke punten van zorg en leiden tot opgeblazen pagina-objecten wanneer ze worden gemengd. De verantwoordelijkheid van Page Object is de toegang tot de staat van de pagina's en de logica van de bewering behoort tot de specificaties.

Paginaobjecttypen

Components vertegenwoordigen de kleinste eenheden en zijn meer gericht, bijvoorbeeld een formulierobject.

Pages combineer meer van deze componenten en zijn abstracties van een volledige pagina.

Ervaringen, zoals je nu al geraden hebt, span de hele stroom over potentieel veel verschillende pagina's. Ze zijn van een hoger niveau. Ze richten zich op de stroom die de gebruiker ervaart terwijl ze met verschillende pagina's communiceren. Een uitcheckflow met een paar stappen is een goed voorbeeld om hierover na te denken.

Wanneer waarom?

Het is een goed idee om dit ontwerppatroon iets later toe te passen in de levenscyclus van een project - wanneer u een beetje complexiteit hebt vergaard in uw featurenspecificaties en wanneer u herhalingspatronen zoals DOM-structuren, geëxtraheerde methoden of andere overeenkomsten kunt identificeren die consistent op uw pagina's.

Dus je moet waarschijnlijk niet meteen beginnen met het schrijven van pagina-objecten. Je benadert deze refactorings geleidelijk wanneer de complexiteit en de omvang van je applicatie / tests groeien. Duplicaties en refactoren die via Pagina-objecten een beter huis nodig hebben, zullen na verloop van tijd gemakkelijker te herkennen zijn.

Mijn aanbeveling is om lokaal te beginnen met het uitpakken van methoden in uw featurenspecificaties. Zodra ze de kritieke massa hebben bereikt, zien ze eruit als overduidelijke kandidaten voor verdere extractie, en de meeste zullen waarschijnlijk passen in het profiel voor Page Objects. Begin klein, want voortijdige optimalisatie laat nare bijtsporen achter!

Laatste gedachten

Paginaobjecten bieden u de mogelijkheid om duidelijkere specificaties te schrijven die beter kunnen worden gelezen en zijn over het algemeen veel expressiever omdat ze van een hoger niveau zijn. Daarnaast bieden ze een mooie abstractie voor iedereen die graag OO-code schrijft. Ze verbergen de details van de DOM en stellen u ook in staat om privémethoden te gebruiken die het vuile werk doen terwijl ze niet worden blootgesteld aan de openbare API. Extractiemethoden in uw featurenspecificaties bieden niet dezelfde luxe. De API van Page Objects hoeft de gedetailleerde Capybara-details niet te delen.

Voor alle scenario's wanneer ontwerpimplementaties veranderen, hoeven uw beschrijvingen van hoe uw app zou moeten werken, niet te veranderen wanneer u Pagina-objecten gebruikt - uw featurespecificaties zijn meer gericht op interacties op gebruikersniveau en geven niet zoveel om de details van de DOM-implementaties. Omdat verandering onvermijdelijk is, worden pagina-objecten kritiek wanneer applicaties groeien en helpen ze ook als de enorme omvang van de toepassing leidt tot een drastische toename van de complexiteit.