In the first post in this series I introduced the Page Object. If you haven’t done so I would suggest you read the first post as we will be building on it in this post. If you want to follow along with the code you can download this zip file that has the code where we left off last time.
In this second post I will introduce two simple concepts that will make our scripts more robust. We will apply one of these enhancements to our scripts taking us further down the path of creating tests that are not brittle. But first we will add one more scenario to our cukes.
Our new Scenario
In order to demonstrate some future techniques I am adding a scenario that performs some data entry. This scenario adds two books to the shopping cart and then checks out with some information.
Scenario: Purchase two books When I purchase "Pragmatic Unit Testing (C#)" And I continue shopping And I purchase "Pragmatic Version Control" And I checkout And I enter "Cheezy" in the name field And I enter "123 Main Street" in the address field And I enter "firstname.lastname@example.org" in the email field And I select "Credit card" from the pay type dropdown And I place my order Then I should see "Thank you for your order"
And of course we have to write a new Page Object to encapsulate the Checkout page.
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 finally we will need a few new step definitions.
When /^I checkout$/ do @shopping_cart.checkout @checkout = CheckoutPage.new(@browser) end When /^I enter "([^\"]*)" in the name field$/ do |name| @checkout.name = name end When /^I enter "([^\"]*)" in the address field$/ do |address| @checkout.address = address end When /^I enter "([^\"]*)" in the email field$/ do |email| @checkout.email = email end When /^I select "([^\"]*)" from the pay type dropdown$/ do |pay_type| @checkout.pay_type = pay_type end When /^I place my order$/ do @checkout.place_order end Then /^I should see "([^\"]*)"$/ do |expected_text| @browser.text.should include expected_text end
Very good. Now I think we are ready to move on.
Eliminating Page Duplication
Many websites have a block of a page duplicated on other pages. For example, it is quite common to have a header and footer section on a site show up on most of the pages. It is also somewhat common to have a side menu show up on all top-level pages.
We do not want to duplicate the code to interact with these reusable page fragments. What can we do to write the code only once and use it in multiple places? Let’s use this page as an example and write some code. This page has a header that appears on all pages as well as some items on the right of each page. Let’s handle the header first.
The header on this page has two menu items: Home and Contact. Let’s create a reusable module that provides access to these menus.
module CheezyWorldHeader def home @browser.link(:text => 'Home').click end def contact @browser.link(:text => 'Contact').click end end
You will notice that this module assumes it has access to an instance variable named
@browser. This shouldn’t be a problem if we only us it in our page objects. But what about the items on the right of the page?
module CheezyWorldWidgets def search_for(value) @browser.text_field(:name => 's').set(value) @browser.button(:value => 'Submit').click end def archives_for(month_label) select_link(month_label) end def category(category_label) select_link(category_label) end private def select_link(label) @browser.link(:text => label).click end end
modules can be used in any page by just requiring them and including them.
require 'cheezy_world_header' require 'cheezy_world_widgets' class CheezyWorldHomepage include CheezyWorldHeader include CheezyWorldWidgets ... end
Although this is a simple example I hope you can see how this will help you eliminate a lot of duplication throughout your pages.
Pages returning Pages
There are two step definitions I would like to bring to your attention. They are the steps in which we have a page transition.
When /^I purchase "([^\"]*)"$/ do |book| @catalog.purchase_book(book) @shopping_cart = ShoppingCartPage.new(@browser) end When /^I checkout$/ do @shopping_cart.checkout @checkout = CheckoutPage.new(@browser) end
In our small example these page transitions seem easy enough. In a larger test suite where we are trying to achieve as much reuse as possible it can often get confusing trying to determine which step causes a page transition. One technique I use that greatly simplifies this situation is having one page object return the next when the transition takes place. This works even better if the method on the page object makes it more obvious that this is taking place. Let’s give it a try here and see how it feels.
Our checkout method on the
ShoppingCartPage is the first method we will look at. We could change this method from
def checkout @browser.link(:text => 'Checkout').click end
def goto_checkout_page @browser.link(:text => 'Checkout').click CheckoutPage.new(@browser) end
and the step definition would change from
When /^I checkout$/ do @shopping_cart.checkout @checkout = CheckoutPage.new(@browser) end
When /^I checkout$/ do @checkout = @shopping_cart.goto_checkout_page end
Using this approach our purchase book step will result in
When /^I purchase "([^\"]*)"$/ do |book| @shopping_cart = @catalog.add_book_to_shopping_cart(book) end
I’ll leave the exercise of changing the page object for you.
There is still much to come in this series on making robust UI tests using Cucumber. In the next post I will introduce a very simple domain specific language that eliminates much of the code we have written to date. Future posts talk about topics like “how to write tests that have large data requirements” and other fun topics.
If you find these posts helpful let me know by posting a comment below. Here is the finished code from this post if you wish to take a look.
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.