This post by Alister Scott originally appeared on watirmelon.com. Please leave all comments on the original article.
Introduction
I was very impressed with Jeff Morgan, known as Cheezy, who recently wrote a series of blog posts about how to use Cucumber and Watir, and shared his code on Github.
I love it when people share their ideas this like, so I thought I would share what I have found useful when setting up a very simple Cucumber with Watir (Celerity & Watir-WebDriver) page object pattern framework, and how this compares to what Jeff has proposed.
Show me the code!
Before we begin, I’ll show you my code. It’s all on Github, right now, as we speak, and you can easily fork and clone this repository to play around with it. It uses a simple google search example.
Project Folder Structure
Google Search Feature
Feature: Google Search
As a casual internet user
I want to find some information about watir, and do a quick conversion
So that I can be knowledgeable being
Scenario: Search for Watir
Given I am on the Google Home Page
When I search for "Watir"
Then I should see at least 100,000 results
Scenario: Do a unit conversion
Given I am on the Google Home Page
When I convert 10 cm to inches
Then I should see the conversion result
"10 centimeters = 3.93700787 inches"
Scenario: Do a search using data specified externally
Given I am on the Google Home Page
When I search for a ridiculously small number of results
Then I should see at most 5 results
Page Object Pattern
For our example, we have two page objects. The page classes delegate any methods that don’t exist on the page to the Browser object that is passed in from Cucumber. This ensures you can call browser specific methods (.title, .url etc.) at the page object level without duplicating methods.
class GoogleHomePage
attr_accessor :search_field, :google_search_button
URLS = { :production => "http://www.google.com/" }
def initialize(browser)
@browser = browser
@search_field = @browser.text_field(:name => "q")
@google_search_button = @browser.button(:name => "btnG")
end
def method_missing(sym, *args, &block)
@browser.send sym, *args, &block
end
def visit
@browser.goto URLS[:production]
end
def page_title
@browser.title
end
def search_for term
self.search_field.set term
self.google_search_button.click
google_results_page = GoogleResultsPage.new(browser)
google_results_page.results.wait_until_present if WEBDRIVER
google_results_page
end
end
Cucumber Initialization
I have everything to do with initialization in support/env.rb. This initializes a browser once, and then uses the same browser instance for each Cucumber scenario. This is different from Jeff’s hooks.rb
file that launches a new browser for every scenario. I have found by reusing the browser, you get much quicker test execution time.
The INDEX_OFFSET is a constant that allows me to use ‘index’ when referring to browser elements across both celerity with its 1-based index and watir-webdriver with its 0-based index.
TEST_DATA_DIR = "./features/test_data"
if ENV["HEADLESS"] then
require "celerity"
browser = Celerity::Browser.new
INDEX_OFFSET = 0
WEBDRIVER = false
else
require 'watir-webdriver'
require 'watir-webdriver/extensions/wait'
browser = Watir::Browser.new :firefox
INDEX_OFFSET = -1
WEBDRIVER = true
end
Before do
@browser = browser
end
at_exit do
browser.close
end
Calling the page objects from the steps
As you can see, most of my steps are in a declarative style, so these normally align pretty well to a method on the page object.
I use a given step to create a page object (in our case @google_home_page), and then call methods on this object for when and then steps. I have created a dynamic invocation of the page creation, so it will work for all pages specified in the cucumber step itself. I use the search_for_term
method to transition between page objects, as this method returns the new page. As a rule of thumb, I try to keep my step definition code to 1 or 2 lines, and at a fairly high level, which makes it much more readable.
Given /^I am on the (.+)$/ do |page_name|
@google_home_page = Object.const_get(page_name.gsub(" ","")).new(@browser)
@google_home_page.visit
end
When /^I search for a? ?"([^"]*)"$/ do |term|
@google_results_page = @google_home_page.search_for term
end
When /^I search for a?n? ?([^"].+[^"])$/ do |term|
term = Common.get_search_term_data term
@google_results_page = @google_home_page.search_for term
end
Then /^I should see at least ([\d,]+) results$/ do |exp_num_results|
@google_results_page.number_search_results.should >= exp_num_results.gsub(",","").to_i
end
Then /^I should see at most ([\d,]+) results$/ do |exp_num_results|
@google_results_page.number_search_results.should <= exp_num_results.gsub(",","").to_i
end
When /^I convert (.+)$/ do |conversion_statement|
@google_results_page = @google_home_page.search_for "convert #{conversion_statement}"
end
Then /^I should see the conversion result "([^"]*)"$/ do |exp_conversion_result|
@google_results_page.conversion_result.text.should == exp_conversion_result
end
Expressing test data external to the features/pages
Unlike Jeff’s recommendation of using default data on page objects, I prefer to store test data externally to the page objects in YAML files. That way I can describe the test data which is directly used in my Cucumber step. For example:
Scenario: Do a search using data specified externally Given I am on the Google Home Page When I search for a ridiculously small number of results Then I should see at most 5 results
The phrase ridiculously small number of results matches directly to a section of my YAML test data file. This keeps the scenario highly readable, with the detailed data specified in the YAML file.
search terms:
ridiculously small number of results:
search term: 'macrocryoglobulinemia marvel'
A common method simply retrieves the appropriate test data, and provides it to the step to use. Note here that, as this step doesn’t contain quotation marks, this implies a description of data, rather than a literal string of data that would contain quotation marks around the string.
When /^I search for a?n? ?([^"].+[^"])$/ do |term|
term = Common.get_search_term_data term
@google_results_page = @google_home_page.search_for term
end
Putting it all together
I have set up two profiles in the cucumber.yml
file, one is the default that uses Firefox under Watir-Webdriver, and one runs Celerity (headless) and is named ci
. It’s then a case of calling "cucumber"
or "cucumber -p ci"
to run each of these profiles.
I have set up a ./go
script that bootstraps the entire process on unix based systems (Mac OSX and Linux).
The final result
Running cucumber
provides results as green as a cucumber.
How does this compare to Cheezy’s framework I mentioned?
- I am using a similar page object pattern to define web application pages, but I haven't created a helper class to mix into each like he has: I simply use instance variables on a page class to define the GUI elements, instead of creating a series of methods for each object.
- My page objects also delegate methods to the main Browser object as required, so you can directly call things like .title, .url etc.
- Instead of using Jeff's default data pattern, I instead prefer to store this data described in external YAML files instead for maximum flexibility.
- Jeff talks about using mixins to mix in common objects. I haven't done this, but it's easily doable with my framework, and should be done as it scales out.
Summary
Jeff has done an excellent job with his series of posts and code (thank you), and I am hoping here to build on that, and show how it’s easy to set up a simple Cucumber & Watir page object pattern framework.