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.
Pingback: Tweets that mention UI Tests – Introducing a simple DSL | CheezyWorld -- Topsy.com
Pingback: A Smattering of Selenium #33 « Official Selenium Blog
I’m implementing your PageObject idea right now! Thanks for the tip!
Pingback: UI Tests – putting it all together | CheezyWorld
Man, this series of posts totally kick ass on addressing UI automated testing common issues!
+1000
Thanks for the encouragement. I hope you enjoy the remainder of this series as well.
Does WatirHelper works with Watir Webdriver ?
Works just fine with Watir Webdriver, i’ve just misconfigured something. Great stuff, waiting for your book.
Good to know.
Pingback: Improving my WatirMelonCucumber page object framework | WatirMelon
Pingback: Introducing page-object gem | CheezyWorld
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!
Abe,
Since posting this entry I’ve created a gem that does everything and a lot more. It does handle frames quite nicely. You can find the gem here -> https://github.com/cheezy/page-object. Please be sure the read the RDocs and the wiki to learn a little about it. Also, a few of my latest posts have introduced some of the features including this post -> http://www.cheezyworld.com/2011/08/08/those-pesky-frames-and-iframes/.
-Cheezy
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.
Working beautifully, now. Thanks!
Great stuff. Started implementing something like this, but using a DSL makes the code much cleaner. Great work.
Joe,
Check out my more recent posts to see a gem that does this plus a lot more.
-Cheezy