Cucumber and Page Objects – how to make Cucumber not be a monumental pain in the arse



Cucumber and Page Objects – how to make Cucumber not be a monumental pain in the arse

0 0


cucumber-and-page-object-presentation


On Github iantruslove / cucumber-and-page-object-presentation

Cucumber and Page Objects

how to make Cucumber not be a monumental pain in the arse

Ian Truslove / ian.truslove@nsidc.org

26 Feb 2013

I didn't invent this.

I don't think Alister Scott did either, but it's from his blog article that I picked up the technique.

Cucumber

Cucumber is a tool for running automated acceptance tests written in a behavior driven development (BDD) style wikipedia

The Good Parts

  • Abstract DSL for writing acceptance tests
  • Implementations in lots of languages
  • High adoption
  • Encourages test composition and reuse

The Bad Parts

  • No namespaces or other organizational constructs
  • Doesn't discourage bad practices
  • Often end up with crappy, unmaintainable code

How To Make It Better

Use the Page Object pattern to better organize your code

A Page Object is an object that represents a web page

...or a service endpoint, or anything else

Show me the code

Nice cucumber

Feature: Smoke tests for metadata feeds harvested by NSIDC's GI-Cat

  Scenario Outline: Check Feed Endpoints
    Given that <Datacenter>'s feed URL is <URL>
    When I access the feed
    Then the response code will be a 200
    And the number of results will be greater than zero

    Scenarios: Feed Endpoints
      | Datacenter | URL                                   |
      | NSIDC      | http://nsidc.org/oai/provider         |
      | CISL       | http://www.aoncadis.org/oai/          |
      | NMI        | http://access.met.no/metamod/oai      |
      | EOL        | http://data.eol.ucar.edu/jedi/catalog |

          

Ugly step definitions

require 'rubygems'
require 'net/http'
require 'open-uri'
require 'nokogiri'

Given /^the feed URL is (.*)$/ do |url|
  @url = url
end

When /^I access the feed$/ do
  @results = Nokogiri::XML(open("#{@url}"))
end

Then /^the response code will be a 200$/ do
  resp = Net::HTTP.get_response(URI.parse(@url))
  resp.code.should == "200"
end

Then /^the number of results will be equal to (.*)$/ do |count|
  @results.xpath('//oai:record', {
    "oai" => "http://www.openarchives.org/OAI/2.0/"}
  ).count.should == count.to_i
end
          

Nice step definitions

Given /^that (.*)'s feed URL is (.*)$/ do |datacenter, url|
  @feed = (datacenter == "EOL" ? Thredds.new(url) : Dif.new(url))
end

When /^I access the feed$/ do
  @feed.access_feed
end

Then /^the response code will be a 200$/ do
  @feed.response_code.should == "200"
end

Then /^the number of results will be greater than zero$/ do
  @feed.num_records.should > 0
end
          

The Feed Page Object

require 'net/http'
require 'open-uri'
require 'nokogiri'

class Feed
 
  def initialize(url)
    @base_url = url.sub(/\/+$/, '')  # strip trailing slashes
  end
  
  def access_feed
    @results = Nokogiri::XML(open("#{@base_url}"))
  end
  
  def response_code
    resp = Net::HTTP.get_response(URI.parse(@base_url))
    resp.code
  end
  
  def get_records(element, namespace)
    @results.xpath("//#{element}", namespace)
  end
end
          

The step definitions for a web app test

Given /^I am on the search page$/ do
  url = ENV["URL"] || "http://integration.nsidc.org/acadis/search"
  @search_page = AcadisSearchPage.new(url, @browser)
end

When /^I search for "(.*?)"$/ do |search_term|
  @search_page.search_for(search_term)
end

When /^I click the next page button$/ do
  @search_page.click_next_page_button
end

Then /^there should be some search results$/ do
 @search_page.results_count.should >= 1
end

Then /^all search results should have a button$/ do
  @search_page.count_data_buttons.should == @search_page.results_count
end

Then /^the page number should be "(.*?)"$/ do |page_number|
  @search_page.current_page_number.should == page_number
end

          

The web page Page Object

(chopped for brevity)

require "watir-webdriver"

class AcadisSearchPage

  def initialize(url, browser=nil)
    @url = url
    @browser = browser or Watir::Browser.new
    @browser.goto @url

    wait_until_loading_is_complete
  end

  def wait_until_loading_is_complete
    @browser.div(:id => 'loading-results').wait_while_present
    @browser.span(:class, 'results-count').wait_until_present
    sleep 0.5
  end

  def search_for(term)
      @browser.text_field(:id, 'keyword').set term
      @browser.button(:id, 'search-now').click
      wait_until_loading_is_complete

      @results_history.push AcadisSearchResultsPage.new(@browser)
  end

  def click_next_page_button
    @browser.a(:class, 'button next').click
    wait_until_loading_is_complete
    @results_history.push AcadisSearchResultsPage.new(@browser)
  end

  # Accessors

  def total_results_count
    last_results_page.total_num_results
  end

  def current_page_number
    last_results_page.current_page_number
  end

  def first_result_date_modified
    last_results_page.first_date_modified
  end
end
          

Benefits of Page Objects

  • Simple pattern, easily applied
  • Much more composable test code
  • Clean test code
    • far more maintainable

Where's the magic?

There's no magic

Basic OO, Interface Segregation Principle

References

http://watirmelon.com/2012/06/04/roll-your-own-page-objects/