Default Data Revisited

I just released a new version of the page-object gem today and it contains a nice new feature. This new feature will make it very easy to apply a set of data to a screen and have it populate all of the fields. This feature, when combined with a new gem I plan to announce soon, will allow for dynamic default data that can be used to drive a web application. This data can be managed within the pages or externally.

This post is an actual section from chapter 5 of my book. It introduces the concept of Default Data and also shows how to use this new feature.

High level tests

Many of the Scenarios we have written so far suffer from a problem. They are too verbose. What I mean by this is they specify every single keystroke even when it is not important for the actual test. Let’s take a look at the original Scenario where we adopted a puppy.


  Scenario: Adopting one puppy
    When I click the View Details button for “Brook”
    And I click the Adopt Me button
    And I click the Complete the Adoption button
    And I enter “Cheezy” in the name field
    And I enter “123 Main Street” in the address field
    And I enter “cheezy@example.com” in the email field
    And I select “Credit card” from the pay with dropdown
    And I click the Place Order button
    Then I should see “Thank you for adopting a puppy!”

What specifically are we verifying? The only thing we are verifying is that the “Thank you” message is displayed. Does it really matter what name I enter in the name field? What about the puppy I select for adoption?

The truth is that none of the data I enter or select on the screens has any impact on the outcome for this particular Scenario. That is also the case with most Scenarios. Although we often have to enter a lot of information in order to complete a Scenario the majority of the information is not directly relevant for the thing we are testing.

A major part of writing good Scenarios is ensuring they are written well and easy to understand. Imagine if we have to traverse through multiple pages to get to the page we plan to tests and each page requires us to enter a lot of data. If we specify all of the typing and clicks in our Scenario there would be a lot of details. It would be very easy to see how one could loose the simple items we wish to specify in all of those details.

Below is the same Scenario as above except all of the unnecessary details are hidden:


  Scenario: Thank you message should be displayed
    When I complete the adoption of a puppy
    Then I should see “Thank you for adopting a puppy!”

This Scenario is much easier to understand and we can clearly see what is being specified. We still need to perform all of the activities necessary to adopt a puppy even though we did not specify them in the Scenario. The next few sections will provide some techniques you can use to enable you to specify your Scenarios at a right level of granularity and still have all of the detailed steps take place.

Inline Tables

In chapter four we saw how we can build up a table of Examples to provide the data for a Scenario Outline. It is also possible to have a table of data that is a part of an individual step. Let’s see how would could place an order using a table of data. We’ll make a modified version of the Scenario from the last section.


  Scenario: Adopting a puppy using a table
    When I click the View Details button for “Brook”
    And I click the Adopt Me button
    And I click the Complete the Adoption button
    And I complete the adoption with:
    | name   | address         | email              | pay_type |
    | Cheezy | 123 Main Street | cheezy@example.com | Check    |
    Then I should see “Thank you for adopting a puppy!”

In this case we’ve collapsed five steps into one. Go ahead and write this Scenario and generate the one new step definition. In this case we will be passing all of our test data to the page in one step. When you generate you step definition you will notice that a table variable is passed into your step. For our purposes we can just call a hashes method on table which will return an Array of Hashes. Each entry in the Array represents one row of data with the key being the table header and the value being the data in the table. Since we only have one row of data we will just call the first method on this Array to get the single Hash containing our data.

We have two options for the implementation. The first option is to use the methods generated by PageObject like this:

When /^I complete the adoption with:$/ do |table|
  data = table.hashes.first
  on_page(CheckoutPage) do |page|
    page.name = data['name']
    page.address = data['address']
    page.email = data['email']
    page.pay_type = data['pay_type']
    page.place_order
  end
end

Although this step definition works fine most of the code seems to be a little out of place in the step definition. The knowledge of how to take a group of data, complete the form, and submit it would seem to be better place in the page object itself. Let’s take a look how that might work.

Here is a new method on CheckoutPage:

  def complete_order(data)
    self.name = data['name']
    self.address = data['address']
    self.email = data['email']
    self.pay_type = data['pay_type']
    place_order
  end

The first thing you might notice is that we are using the keyword self in our new method. The reason for this is that equals methods (one that end with an equal sign) are required to have a receiver so they are not mistaken for local variable assignment. self represents the current object and therefore when we call self.name = we are really saying call the name= method on this same object.

With this method on our page object our step definition can be simplified to this:

When /^I complete the adoption with:$/ do |table|
  on_page(CheckoutPage).complete_order(table.hashes.first)
end

DO NOT OVERUSE TABLES
Tables are nice and do help us consolidate multiple steps into one but they also make things a little harder to read. Use you best judgement on when you believe it adds to the overall Scenario and make sure you do not overuse them to the point where it takes away from what we are trying to say.

Default Data

It is fairly common for your Scenarios to require a lot of data in order to complete a successful run. In the last section we discussed how to use tables to create higher level tests. In that Scenario it really didn’t matter if we used the name “Cheezy” or “Mickey Mouse”; the outcome would be the same. In fact, none of the data we provide made any difference on the outcome. As long as we select a puppy and complete the checkout form we see the thank you message.

We should ask ourselves why do we need to provide all of this data if it is not important to what we are testing. Does providing this data really add clarity or is it distracting? Does specifying every click necessary to traverse through the system add to our understanding? Does it make the specification more complete?

If we were able to rephrase the Scenario from the last section to state the exact essence of what we were trying to specify it would be this:


  Scenario: Thank you message should be displayed
    When I complete the adoption of a puppy
    Then I should see “Thank you for adopting a puppy!”

This is much cleaner and easier to understand. It specifies the exact thing we are trying to describe – a thank you message should be displayed when the adoption is completed.

In order to implement the Scenario above we still need to provide the information necessary to fill in and submit the pages of our application. But where will it come from? This is where Default Data comes in.

Default Data is a widely used pattern in the testing community. The pattern is implemented by providing a set of data that can be used to generically traverse through the application but at the same time allowing you to override any data specific to your context. For example, take the last Scenario. In that Scenario we should just be able to use the Default Data to complete all of the pages and the validate the message. If required us to use a “Credit card” then we should be able to specify it override the default value and use “Credit card”. Let’s see how we could implement this in our CheckoutPage.

class CheckoutPage
  include PageObject

  DEFAULT_DATA = {
    ‘name’ => ‘cheezy’,
    ‘address’ => ‘123 Main Street’,
    ‘email’ => ‘cheezy@example.com’,
    ‘pay_type’ => ‘Purchase order’
  }

  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 complete_order(data = {})
    data = DEFAULT_DATA.merge(data)
    self.name = data['name']
    self.address = data['address']
    self.email = data['email']
    self.pay_type = data['pay_type']
    place_order
  end
end

There are several new things here to discuss. The first is the Hash near the top of the class. It simply provides the default data that will be used by the page. The way this happens is that the first line of the complete_order method merges the data passed to the method with the default data of the page. The merge simply tries to match up a key and if a match is found it will update the corresponding value.

The other new thing here is the modified parameter passed to our complete_order method – data = {}. This is Rubies way of specifying a default parameter to a method. If we provide a parameter when we call this method then it is passed on through. If we call the method without passing a parameter the default will be used which is an empty Hash is this case. The effect of using the empty Hash is to just use all of the default data.

If you are using PageObject 0.5.3 or higher there is an even simpler way to populate your page with a Hash of data. A new method has been added named populate_page_with which takes a Hash. It will match up the keys from the Hash with the names you provided for the elements when you declared them on the page. All values must be Strings except for Checkboxes and Radio Buttons must be true or false. Let’s look at our complete_order method if we use this instead.

When /^I complete the adoption using a Credit card$/ do
  on_page(CheckoutPage).complete_order(‘pay_type' => 'Credit card')
end

When /^I complete the adoption$/ do
  on_page(CheckoutPage).complete_order
end

I think we have achieved the goal of default data. We can now individually set the values on the page using the methods generated by page-object. We can use the default data by calling the complete_order method passing no parameters. We can use partial or no default data by passing in a Hash that contains the data we wish to use.

The only additional page we have in our system where we have to provide data is the HomePage. Providing default data for this page is as simple as providing a default parameter for the adopt method.

def adopt(name = ‘Brook’)
  index = puppy_index_for(name)
  button_element(:value => 'View Details', :index => index).click
end

With this in place we can now get back and add the Scenario we introduced at the beginning of this section and add the missing step definition.

When /^I complete the adoption of a puppy$/ do
  on_page(HomePage).adopt
  on_page(DetailsPage).adopt_me
  on_page(ShoppingCartPage).complete_adoption
  on_page(CheckoutPage).complete_order
end

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>