UI Tests – Introducing a simple DSL

In the first and second posts of this series we introduced Page Objects and evolved them to include page partials and return the next page object. In this post we will introduce a simple domain specific language that will eliminate a lot of the annoying repetitive work we have done so far. After we look at the DSL we will refactor our tests to take advantage of its’ capabilities. When we are finished our scripts will be cleaner than when we left off at the end of the last post.

WatirHelper

WatirHelper is a very simple DSL that adds methods to your page objects that perform routine tasks such as setting data in a text_field and clicking a button. I created this DSL over the course of two years while coaching teams on agile practices. I usually recommend teams implement ATDD and when they do I help the testers automate the tests with Cucumber. It has had extensive usage with Watir and FireWatir. It does not currently work with SafariWatir but I am working on it. I have not tested it with Celerity but intend to do so soon. It is fairly well commented so you should have no problem getting up to speed quickly. I am introducing a fairly stripped down version here but the full version will be available in a book that I hope to release after the first of the year.

WatirHelper is implemented as a simple module that you include in your page objects. It then adds methods that you can use to define your page and access its’ elements. For example, it will add a text_field class method to your page object. Let’s look at how we would call this in the CheckoutPage class for the name field from the last post.

  
class CheckoutPage
  include WatirHelper

  text_field(:name, :id => 'order_name')
  ...

  def initialize(browser)
    @browser = browser
  end

  ...
end

It looks nice but what does it do for us? Let’s take a look at the text_field method to start to understand the benefit of using this domain specific language. Here is the method from WatirHelper:

  
def text_field(name, identifier)
  identifier = make_safe_identifier(identifier)
  define_method(name) do
    @browser.text_field(identifier).value
  end
  define_method("#{name}=") do |value|
    @browser.text_field(identifier).set(value)
  end
  define_method("#{name}_text_field") do
    @browser.text_field(identifier)
  end
end

On line 3 it is adding a method with the name passed in – in our case name. This method returns the value contained in the text_field. On line 6 it adds another method that sets the value in the text_field. And finally on line 9 it adds another method that returns the Watir TextField object. So adding

  
  text_field(:name, :id => 'order_name')

causes WatirHelper to add the following three methods to our page object:

  
  def name
    @browser.text_field(identifier).value
  end
  
  def name=(value)
    @browser.text_field(identifier).set(value)
  end
  
  def name_text_field
    @browser.text_field(identifier)
  end

Let’s look at one more example to give you another view of its’ power. Adding

  
  button(:place_order, :value => 'Place Order')

causes WatirHelper to add the following three methods to our page object:

  
  def place_order
    @browser.button(identifier).click
  end

  def place_order_no_wait
    @browser.button(identifier).click_no_wait
  end

  def place_order_button
    @browser.button(identifier)
  end

Let’s put it to use

The best way to understand WatirHelper‘s power is to see it in action. I will begin by re-writing the CheckoutPage using WatirHelper. The original class had a method for each element on the page. Here it is:

  
class CheckoutPage

  def initialize(browser)
    @browser = browser
  end

  def name=(name)
    @browser.text_field(:id => 'order_name').set(name)
  end

  def address=(address)
    @browser.text_field(:id => 'order_address').set(address)
  end

  def email=(email)
    @browser.text_field(:id => 'order_email').set(email)
  end

  def pay_type=(pay_type)
    @browser.select_list(:id => 'order_pay_type').set(pay_type)
  end

  def place_order
    @browser.button(:value => 'Place Order').click
  end
end

And now it is time to look at the new and improved version of CheckoutPage.

  
class CheckoutPage
  include WatirHelper

  text_field(:name, :id => 'order_name')
  text_field(:address, :id => 'order_address')
  text_field(:email, :id => 'order_email')
  select_list(:pay_type, :id => 'order_pay_type')
  button(:place_order, :value => 'Place Order')

  def initialize(browser)
    @browser = browser
  end
end

As you can see we have eliminated a lot of boilerplate code. The new version is significantly shorter than the previous. But what does this do to our ShoppingCartPage? Let’s take a look:

  
class ShoppingCartPage
  include WatirHelper

  QUANTITY_COLUMN = 1
  DESCRIPTION_COLUMN = 2
  EACH_COLUMN = 3
  TOTAL_COLUMN = 4
  HEADER_OFFSET = 2

  link(:checkout, :text => 'Checkout')
  link(:continue_shopping, :text => 'Continue shopping')
  cell(:cart_total, :class => 'total-cell')
  table(:shopping_cart, :index => 1)

  def initialize(browser)
    @browser = browser
  end

  def goto_checkout_page
    checkout
    CheckoutPage.new(@browser)
  end

  def quantity_for_line(line_number)
    cart_data_for_line(line_number)[QUANTITY_COLUMN].text
  end

  def description_for_line(line_number)
    cart_data_for_line(line_number)[DESCRIPTION_COLUMN].text
  end

  def each_for_line(line_number)
    cart_data_for_line(line_number)[EACH_COLUMN].text
  end

  def total_for_line(line_number)
    cart_data_for_line(line_number)[TOTAL_COLUMN].text
  end

  private

  def cart_data_for_line(line_number)
    shopping_cart[HEADER_OFFSET+line_number]
  end
end

Again, we have a much smaller and cleaner page object.

How does this change the way I work?

In the final post in this series I will talk extensively about the ATDD workflow and how that effects when and how we write the code. I will mention a couple of things here that I think are important and directly relate to WatirHelper.

First of all we can completely decouple the creation of the page objects from the creation of the step definitions that use them. As soon as the screen mock-ups are complete you can quickly create a page object that contains the visible elements on the page. If the mock-up goes through changes it is very easy to make the corresponding change in the page object.

Also, the post development sync is much easier when using WatirHelper. This is the time when the developer is completing the code and we need to synch the test with the code. Items we might find here are mis-identified element types (we thought it was a button but it ended up being a link with an image) and incorrect locators (we supplied the wrong :id). As you can imagine, both of these situations are incredibly easy to resolve with WatirHelper

What’s next?

We are now half way through this series on writing UI tests. The remaining posts in this series will continue to build upon what we have written so far. Each post will introduce one or more concepts that will help us make our tests more robust and maintainable. The next post will talk about handling large amounts of data in your scripts and building higher-level tests. I hope you stay aboard for the journey.

By the way, if you want to look at the entire WatirHelper module from this posting you can find it in the features/support directory of this posts finished codebase.

Update: The code for this series is now available in github. You can access it here. There is a branch for each checkpoint in this series with the latest on master.

26 thoughts on “UI Tests – Introducing a simple DSL

  1. Pingback: Tweets that mention UI Tests – Introducing a simple DSL | CheezyWorld -- Topsy.com

  2. Pingback: A Smattering of Selenium #33 « Official Selenium Blog

  3. Pingback: UI Tests – putting it all together | CheezyWorld

  4. Pingback: Improving my WatirMelonCucumber page object framework | WatirMelon

  5. Pingback: Introducing page-object gem | CheezyWorld

  6. This is great stuff. I do have one question, though: within this framework, how do you identify things that are nested inside of iframes, for example? Is that possible?

    If there’s a way, it’s not obvious to me.

    Thanks for your help!

  7. Cheezy, I’m getting this error:

    /Users/abrahamheward/.rvm/gems/ruby-1.9.2-p290/gems/page-object-0.3.1/lib/page-object/accessors.rb:218:in `block in link’: undefined method `click_link_for’ for nil:NilClass (NoMethodError)

    With this code:

    class Home
    def initialize(browser)
    @browser = browser
    end
    include PageObject
    link(:site_editor, :text=>”Site Editor”)
    end
    home = Home.new(browser)
    home.site_editor

    What’s going wrong?

    • Abe,

      Please do not create the initialize method. The PageObject gem already has one and it performs many additional duties. Just remove the method and it should work appropriately.

      • If you need a creation hook then you can create an initialize_page method. It is a callback that is called after the page is initialized and prior to traversing to the page if you are using the page factory with the visit_page method.

  8. Is there a way to work with a long list of links without having to list all of them as page individual page objects. I would like to be able to pass the link text to the page object call as a variable.

    • It is very possible to do this but I think you are missing the point for even having page objects. The purpose for page objects it to abstract the things on the page from the tests. If you pass the link text you are not building the abstraction and you are creating a brittle test.

      Anyway, here is some code that will return a list of links on the page that all have the same class:

      links(:product, :class => ‘product’)

      With this declaration you now have a method named `product_elements` that will return all of those links as an `Array`.

  9. pardon, may be this is not the right spot to raise my question.

    the question goes like this:

    I want to check existence of div element on the fly by using some attributes passed from the feature file, but I tied to use div_element(:id=>’xxx’)? , I’m getting error.

    any advise is helpful.
    thanks

  10. hey Cheezy,

    I’m back again, is there any google discussion going on for Page Object Model gem you’ve developed so far? it would be so much helpful to toss question back and forth over there i think.

    Thanks

  11. Hi Cheezy

    Have all the Accessor methods that check for the existence (methods ending in ?) of the element been implemented?

    e.g. table(:Table, :css => “table”)

    when I try to run the Table? method it says (note I am using jRuby and selenium):

    …RuntimeError: exists? not available on table element
    exists? at /Users/zsethna/.rvm/gems/jruby-1.7.8/gems/page-object-0.9.5/lib/page-object/platforms/selenium_webdriver/table.rb:32
    Table? at /Users/zsethna/.rvm/gems/jruby-1.7.8/gems/page-object-0.9.5/lib/page-object/accessors.rb:1226
    call at org/jruby/RubyMethod.java:124

    Any ideas on whats happening here?
    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>