I’ve been giving some thought about the maintainability of having local page object model references in Cucumber steps. To explain what I mean, here’s some code of mine from an Etsy page model step:
When /^an item is added to the cart$/ do @advanced_search_page = EtsyAdvancedSearchPage.new @browser, true @search_results_page = @advanced_search_page.search_for 'hat' @etsy_item_page = @search_results_page.click_first_result @item_title = @etsy_item_page.item_title @etsy_cart_page = @etsy_item_page.click_add_to_cart end
Here I am using instance variables in Cucumber steps to store references to pages that I am interacting with. As you can see, it’s quite easy to lose track of which page is which.
If I want to use one of these pages in another step, I have to refer to the same variable name. The key to this concept is having pages return pages, so you don’t have to initialize pages in your steps (except for the first visit, which normally happens only once per scenario).
A colleague and good friend of mine has been showing me some alternatives. One alternative to this is to actually dynamically refer to pages only when you need them. This means you don’t need to return a page from a page, as they are always initialized when needed. The above method would look like:
When /^an item is added to the cart$/ do visit EtsyAdvancedSearchPage do |page| page.search_for 'hat' end on EtsySearchResultsPage do |page| page.click_first_result end on EtsyItemPage do |page| @item_title = page.item_title page.click_add_to_cart end end
This introduces two new methods at the Cucumber step level that need to be created: on, and visit. The on method simply creates a new page object so you can call methods from it. As each page initialize checks expected title and expected element, it will raise an error automatically if either are incorrect. The visit method is an extension of on, which actually initalizes the page and visits it.
These are defined as a module in our Cucumber env.rb file, and then mixed into the Cucumber World, so that steps automatically have access to these.
module Browser BROWSER = Watir::Browser.new ENV['WEB_DRIVER'] ||:firefox def visit page_class, &block on page_class, true, &block end def on page_class, visit=false, &block page = page_class.new BROWSER, visit block.call page if block page end end World Browser
Summary
By introducing the on and visit methods, it means that we no longer need to have instance variables for page classes in our Cucumber steps. We also no longer need to worry about state transformations in pages, as these can be done in a modular way in the steps themselves. It means that when an error occurs initializing a page, it is more likely to to occur in the correct place. I find the steps more readible, as you only have to initialize the page once using the on block, and then can refer to page.
I have updated my set of Etsy Watir-WebDriver steps if you’d like to take a look.
What do you think? How will this scale?
Update: I have updated the WatirMelonCucumber project to use this style. It’s slightly different in that it supports two sites, and therefore the on method dynamically switches between these.
Cucumber Steps:
When /^I search for a? ?"([^"]*)"$/ do |term| on :home do |page| page.search_for term end end
The on method:
def on page, visit=false, &block page_class = Object.const_get "#{@site}#{page.capitalize}Page" page = page_class.new BROWSER, visit block.call page if block page end
