Roll your own page objects

There seems to be a lot of focus being put into page object ruby gems at the moment. Cheezy has done a fantastic job of the aptly named page-object that supports Watir-Webdriver and Selenium-Webdriver, and then there’s the more recent site_prism (also fantastic) by Nat Ritmeyer that works with Capybara. Before these two came along, I even wrote my own; the now retired watir-page-helper gem.

The premise of these gems is they make it super easy to create page objects for your ruby automated testing projects. But today I want to discuss another crazy idea with you: do you even need a gem at all to do page objects?

Background

I recently refactored some automated tests that Chris McMahon wrote as a potential framework for Wikimedia Foundation (creators of Wikipedia). Chris’s code used Cheezy’s excellent page-object gem so I happily went about my refactoring his code using that gem. Suddenly… I found instead of helping me it started to hinder me. I kept having to refer to page-object user guide I got from Cheezy in Austin to work out how things work. Namely:

  1. How to define elements: as they are different from watir-webdriver (eg. list_item vs li, cell vs td etc.)
  2. How to identify elements: as they are limited to certain supported attributes by element type, unlike watir-webdriver which supports every attribute for all elements
  3. What methods each element provides and what each does: as different elements create different methods with different behaviours, so calling a link element clicks it, whilst a span element returns its text.

The main problem I personally found was that Page-object has essentially created its own DSL to describe elements in page objects, and this DSL is subtly and not so subtly different from the Watir-Webdriver API, so the API I know and love doesn’t work in a lot of places.

An example. There’s a common menu bar on all the Wikimedia sites that displays the logged in user as a link (to the user’s page).

The link is only recognizable by its title attribute, and whilst this is supported by watir-webdriver (it supports any attribute), it is not supported by page-object. The source html looks like:

<a class="new" accesskey="." title="Your user page [ctrl-.]" href="/wiki/User:Alister.scott">Alister.scott</a>

What I would have liked to do was:

  link :logged_in_as, :title => 'Your user page [ctrl-.]'

But, instead, I had to do this (which isn’t very DRY):

  def logged_in_as
    @browser.link(:title => 'Your user page [ctrl-.]').text
  end

I believe essentially what has happened is the page-object, in its neutrality between selenium-webdriver and watir-webdriver, has created its own DSL that is somewhat of a halfway point between the two. This is probably fine for most people starting out, it’s just an API to learn, but for someone like me who has extensive experience with the watir-webdriver API (and loves the power of it), I find it limiting. This is particularly evident when I write a majority of my code using the watir-webdriver API under IRB.

So, I had to take a re-think. Why not roll my own page objects for Wikimedia Foundation?

Roll your own page objects

I recently had a discussion with a colleague/good friend about page objects which went along the lines of “I don’t understand those page object gems because you end up writing a custom page object pattern for each project anyway, as every project/application you work on is different in its own way”. It was one of those aha moments for me.

What I needed was to roll my own Wikimedia page objects.

Taking it back to basics, essentially there are three functions I see a page object pattern provides:

  1. Ability to easily instantiate pages in a consistent manner;
  2. Ability to concisely describe elements on a page, keep it DRY by avoiding repetition of element identifiers (using the underlying driver’s API); and
  3. Ability to provide higher level methods that use the elements to perform user oriented functions.

You can probably notice the helper methods – the magic – that gems like page-object and site_prism provide are missing from my list. This is on purpose, and is because, after lots of thought, I actually don’t find these useful, as they encourage specifications/steps to be too lower level. I would rather a high level method on the page (eg. login) than exposing my username and password fields and a login button.

A Wikimedia Page Model

Taking those things into consideration: this is the page model I came up with for Wikimedia.

Generic Base Page Class

The generic base page class is what everything else extends. It contains the instantiation code common to all pages, and the class methods needed to define elements and methods (more on these later).

Wikimedia Base Page Class

This page class contains elements and methods that are common to all Wikimedia pages. The ‘logged in user’ example above is a good example of something that is the same on every Wikimedia page, whether you’re on Wikipedia, or Wikimedia Commons etc.

Commons & Wikipedia Base Page Classes

These two classes are placeholders for elements and methods are common to a particular site. At the moment with my limited examples, these don’t contain content.

Commons & Wikipedia Page Classes

These are the actual pages that are representations of pages in Wikimedia. These are in separate modules so they are in different namespaces (you can have a Wikipedia::LogonPage and a Commons::LogonPage).

Some example pages:

class Wikipedia::LogoutPage < Wikipedia::BasePage
  page_url "#{Wikipedia::BASE_URL}/w/index.php?title=Special:UserLogout"
  expected_title "Log out - #{Wikipedia::TITLE}"

  element(:main_content_div) { |b| b.div(id: 'mw-content-text' ) }
  value(:main_content) { |p| p.main_content_div.text }
end

Here we can see we define a page_url and expected_title, which are used to instantiate the page.

Next we define an element passing in a block of watir-webdriver code for it, and a value by referencing the element we defined before it. Since these element and value methods execute blocks against self, and the class delegates missing methods to our browser, we can refer to either the browser (shown as b) or the page class (shown as p) in our blocks.

class Commons::LoginPage < Commons::BasePage
  page_url "#{Commons::BASE_URL}/w/index.php?title=Special:UserLogin"
  expected_title "Log in / create account - #{Commons::TITLE}"

  login_elements
  value(:logged_in?) { |p| p.logged_in.exists? }

  def login_with username, password
    username_field.set username
    password_field.set password
    login_button.click
  end
end

In this example, we again define the page_url and expected_title, but we have stored the login_elements with the WikimediaBasePage (as they are the same across all the sites) so we include them by specifying login_elements. We have also defined a login_with method that performs actions on our elements.

There are three available methods to define page elements, values and actions, and these all follow the same format of specifying the method name, and passing in a block of watir-webdriver code.

Calling the page objects from Cucumber step definitions

I chose to use Cucumber for the Wikimedia Foundation framework over Chris’s choice of RSpec as I find it easier to specify end-to-end tests in this way. I find the Cucumber step definitions encourage reuse of steps typically used to set up a test (that are often duplicated in RSpec).

I try to stick to calling the exposed methods, values or actions instead of the elements themselves from my Cucumber steps to ensure I am writing them at a high level. An example step using the page above looks like:

Given /^I am logged into Commons$/ do
  visit Commons::LoginPage do |page|
    page.login_with Commons::USERNAME, Commons::PASSWORD
    page.should be_logged_in
  end
end

The visit and on methods are defined in a Pages module that is mixed into the Cucumber World so these available on all step definitions. As named, the visit method instantiates and visits the page, whereas the on just instantiates it.

module Pages
  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

Summary

That’s all there really is the rolling your own page objects. I found this excercise useful as it gives me maximum flexibility and allows me to clearly define pages how I want to define them. I appreciate all the great work that Cheezy and Nat have done on their page object gems, if anything these contain great inspirations on how to roll your own custom page objects most suited to your environment and applications.

You can check out my full code here on Github.

14 thoughts on “Roll your own page objects

  1. I just added the ability to find a link by title. When you find deficiencies like this in the future please ping me so I know what needs to be added.

    Thanks
    -Cheezy

  2. Hello Coworker :)

    I am doing that too, it is called Cello (https://github.com/camiloribeiro/cello) it is not that beautiful as the examples you said, but I am trying to implement something you can customize the DSL to something that can help your QAs to write tests faster, independent of the locale, tools and languages that they are already in use.

    For while it is pretty simple and I am starting the first big challenge now, that is create the DLS file and the second one, deal with the browser . . . I think I will need on month more to finish the first “usable” version, but I would love some early feedbacks in this first features.

    Another important point is that I am trying to create an interface more user-driving, for exemplo, instead have a element for each radio button, I created an interface to deal with the whole radio collection as a user normally does.

    I would love if you take a look on it :)

    Thanks

  3. Hi Alister,

    Again, spot on: If the options out there don’t fit what you need, roll your own! That’s why I wrote SitePrism – there wasn’t anything out there that did the job for me.

    BTW, SitePrism is tiny – it barely justifies an existence – what you’ve written replicates most of what SitePrism does. You’ll be pleased to know that I use all of the functionality of SitePrism on my current project, but that’s only because I’ve only added functions to SitePrism that I find myself needing! Being a fan of minimalist software, SitePrism is kept small – there’s nothing in there that I don’t use. In fact, looking at your 3 characteristics of a page object API (with which I agree entirely), SitePrism does those and almost nothing more!

  4. I’ve been a die-hard fan of cheezy’s page-object gem for months, now, but I have to say that I agree with a lot of the annoyances you cite, here. This idea has my head spinning. Bravo, Alister!

  5. Two things:

    First an advisory to anyone looking at the sample code who like many new users of Watir is fairly new to Ruby coding. Alister is making use of the new ruby 1.9 alternate syntax for defining a hash when the key is a symbol (which looks a little bit like a YAML file actually). This means the sample code will not run on ruby 1.8 (if you are working somewhere that is using that version). If you get confused by seeing something like this
    b.text_field(id: ‘wpName1′)
    understand that it is the same as writing
    b.text_field(:id => ‘wpName1′)

    Secondly, Cheesy (if you are following these comments) what’s the best way for us to ping you with issues? or perhaps to arrange some training on cuke/watir/po’s

    well ok Three things
    Third, Alister, great stuff, great food for thought, you are causing Abe and I to evaluate the approach. Love your sample code, it always seems to clean and DRY compared to my own

  6. Hi Alister,

    You did an excellent job in writing these page object classes. I learn a lot from your Wikipedia prototype application.

    Meanwhile, I would like to discuss with your the parameters in page object classes.

    In your prototype, you choose to use Random page to show a random wikipedia page. In the real world, the wikipedia page’s url contains the term. For example, http://en.wikipedia.org/wiki/Web_2.0.
    To use the same article page object for different terms, it is necessary to pass a parameter while generating the page object from the pagefactory and modify the generic page object to accept parameters.

    A more complex url is like this:

    http://www.bestbuy.com/site/Samsung+-+51%26%2334%3B+Class+/+Plasma+/+720p+/+600Hz+/+HDTV/4834361.p?id=1218540193787&skuId=4834361

    I believe we can find more examples like this, especially for RoR applications.

    What is your opinion about this issue?

    Thanks
    Jun

  7. I love the element_generator. In incorporated this into my new project and changed it to automatically decorate the elements with when_present using the sourcify gem like this…

    def self.element element_name, timeout=10, &block
    if timeout == 0
    element_decorator element_name, &block
    else
    f = block.to_source
    block = eval(f[0..-3] + “.when_present(#{timeout})}”)
    element_decorator element_name, &block
    end
    end

    def self.element_decorator element_name, &block
    define_method element_name.to_s do
    yield self
    end
    end

  8. Alister,

    I’m new to page object land, but I’m trying to roll my own page objects myself – I find your arguments for doing do very persuasive.

    However, I’m trying to model a framework largely taking after what you’ve done here but am having a hard time getting it off the ground. I’ve put up what I have so far on my github page here:

    When I do my initial cucumber run (“bundle exec rake features”) which would ideally generate the step outlines, I run into a problem that suggests that I’ve gotten the plumbing wrong somewhere: https://github.com/TheMetalCode/autocity_github

    “uninitialized constant Plaza (NameError)
    /Users/jasonhagglund/Sites/autocity_github/lib/pages/plaza_base_page.rb:1:in `’
    /Users/jasonhagglund/Sites/autocity_github/lib/pages.rb:3:in `require’
    /Users/jasonhagglund/Sites/autocity_github/lib/pages.rb:3:in `’
    /Users/jasonhagglund/Sites/autocity_github/features/support/env.rb:9:in `require’
    /Users/jasonhagglund/Sites/autocity_github/features/support/env.rb:9:in `’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/rb_support/rb_language.rb:129:in `load’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/rb_support/rb_language.rb:129:in `load_code_file’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:171:in `load_file’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:83:in `block in load_files!’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:82:in `each’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:82:in `load_files!’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime.rb:175:in `load_step_definitions’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/runtime.rb:40:in `run!’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/cli/main.rb:43:in `execute!’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/lib/cucumber/cli/main.rb:20:in `execute’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/gems/cucumber-1.2.1/bin/cucumber:14:in `’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/bin/cucumber:19:in `load’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/bin/cucumber:19:in `’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/bin/ruby_noexec_wrapper:14:in `eval’
    /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@autocity/bin/ruby_noexec_wrapper:14:in `’
    rake aborted!”

    Any pointers here would be greatly appreciated. Thanks

    • Just to follow up: I went ahead and forked your code as-is, installed the bundle, and tried to run the features just to see how it would behave.

      I didn’t get the uninitialized constant, but I did get a failure to obtain a stable firefox connection:
      unable to obtain stable firefox connection in 60 seconds (127.0.0.1:7055) (Selenium::WebDriver::Error::WebDriverError)
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/firefox/launcher.rb:79:in `connect_until_stable’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/firefox/launcher.rb:37:in `block in launch’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/firefox/socket_lock.rb:20:in `locked’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/firefox/launcher.rb:32:in `launch’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/firefox/bridge.rb:20:in `initialize’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/common/driver.rb:31:in `new’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver/common/driver.rb:31:in `for’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/selenium-webdriver-2.24.0/lib/selenium/webdriver.rb:65:in `for’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/watir-webdriver-0.6.1/lib/watir-webdriver/browser.rb:35:in `initialize’
      /Users/jasonhagglund/Sites/test_page_objects/features/support/env.rb:9:in `new’
      /Users/jasonhagglund/Sites/test_page_objects/features/support/env.rb:9:in `’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/rb_support/rb_language.rb:129:in `load’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/rb_support/rb_language.rb:129:in `load_code_file’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:171:in `load_file’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:83:in `block in load_files!’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:82:in `each’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime/support_code.rb:82:in `load_files!’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime.rb:175:in `load_step_definitions’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/runtime.rb:40:in `run!’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/cli/main.rb:43:in `execute!’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/lib/cucumber/cli/main.rb:20:in `execute’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/gems/cucumber-1.2.1/bin/cucumber:14:in `’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/bin/cucumber:23:in `load’
      /Users/jasonhagglund/.rvm/gems/ruby-1.9.3-p286@wmf-custom-page-object/bin/cucumber:23:in `’

      • This error is because it’s using an old version of watir webdriver against a new version of Firefox. Run ‘bundle update’ and it’ll fix the issue.

    • OK, after a little more poking around, I actually got my implementation to work.

      First, the order in which I was requiring the modules in env.rb was off. Once I corrected the order to load ‘page_helper’, then my site-specific modules, then ‘pages’, I stopped getting the uninitialized constant error.

      Furthermore, I’m able to run my smoke-test login feature, so I think this approach is likely to work out for me after all. Which means I probably don’t have much of a question anymore. :)

  9. Inspired by this blog posting, the simple framework for page objects (and data objects) code that Abe and I (MOSTLY Abe!) created for the Kuali Student project has recently been turned into a gem called test-factory, so if you like this approach it might bear looking at.

    Last I looked Abe’s fork had the most recent code updates https://github.com/aheward/TestFactory

Comments are closed.