Writing a UI Automation Framework – Part 2

Part 1 is here

In Our Last Episode

Last time, we talked about the technology stack for writing an example framework–the stack includes Watir, Ruby and Cucumber.

This time we’ll start off writing some actual framework code, and carve through a test case as well. Maybe two or three.

Main Screen Turn On

First we’re gonna get a directory set up with Cucumber’s format. Open up a terminal window:

start command prompt with ruby

Then create a directory where you want the project to be. It can be anywhere–mine’s called Testzius Framework. Then “cd” into that directory:

> mkdir "Testzius Framework"
> cd "Testzius Framework"

To get this directory set up to Cucumber’s liking, type “cucumber –init”. This will create a short directory structure at this location:

> cucumber --init
  create   features
  create   features/step_definitions
  create   features/support
  create   features/support/env.rb

The features/ directory is where we’re going to be spending some time soon.

Describing the Test

Now we’re going to start out by writing a feature file.

A “feature” in Cucumber is a way to describe what the test is going to do, by saying what preconditions you need, what actions you perform and what your expected output is.

A feature contains one or more test cases, called “Scenarios”.

Let’s switch over to a text editor and create a new feature file called “flights.feature”, in the features directory:

flights feature file

We can start by letting Cucumber do some of the lifting for us, by generating some stubs for us to write our automation into. So just a really simple test case to check and see if the cost of certain flights are correct:

Feature: New Tours Flights Page Tests

Scenario: Check Flights and Prices
Given I go to "http://newtours.demoaut.com/index.php"
When I click the Flights link
Then the flight from "Atlanta" to "Las Vegas" should cost "$398"
Then the flight from "Boston" to "San Francisco" should cost "$513"
# Then the flight from "Los Angeles" to "Chicago" should cost "$168"
# Then the flight from "New York" to "Chicago" should cost "$198"
Then the flight from "Phoenix" to "San Francisco" should cost "$213"

(I’ve left the Los Angeles -> Chicago and New York -> Chicago because these are formatted a little differently in the DOM, and finding the elements with that data is a little out of scope of this article–it’d be more suited for an advanced xpath tutorial. Also, Go Cardinals!)

When we run it, Cucumber will complain that these steps aren’t defined. Which, that’s fine–we can use that to our advantage by copying them straight from the terminal and putting them into the framework.

Kick off a run by typing “cucumber” in the framework directory, and you’ll get this:

You can implement step definitions for undefined steps with these snippets:

Given(/^I go to "([^"]*)"$/) do |arg1|
 pending # Write code here that turns the phrase above into concrete actions
end

When(/^I click the Flights link$/) do
 pending # Write code here that turns the phrase above into concrete actions
end

Then(/^the flight from "([^"]*)" to "([^"]*)" should cost "([^"]*)"$/) do |arg1, arg2, arg3|
 pending # Write code here that turns the phrase above into concrete actions
end

One cool thing that Cucumber does for us is, if we have parts of a step enclosed in double quotes, it automatically treats those portions as a variable–this is why you get “([^”]*)” in the step–that’s a regular expression for a string within a pair of double quotes.

So let’s grab those and put them in a file called “steps.rb”, under the step_definitions folder:

step definitions for framework

Now the steps.rb file should contain:

Given(/^I go to "([^"]*)"$/) do |arg1|
 pending # Write code here that turns the phrase above into concrete actions
end

When(/^I click the Flights link$/) do
 pending # Write code here that turns the phrase above into concrete actions
end

Then(/^the flight from "([^"]*)" to "([^"]*)" should cost "([^"]*)"$/) do |arg1, arg2, arg3|
 pending # Write code here that turns the phrase above into concrete actions
end

Giving It Color

All right, before we fill these guys out, we’ll need a little bit of boilerplate for things like starting up a browser instance and navigating to a url. Let’s put that up at the top:

require 'rubygems'
require 'watir-webdriver'

def go_to_url(url)
 load_link($timeout_length){ $browser.goto url }
end

def load_link(waittime)
 begin
 Timeout::timeout(waittime) do
 yield
 end
 rescue Timeout::Error => e
 fail "Page load timed out: #{e}"
 retry
 end
end

def initialize_browser(type=:ff)
  if $browser == nil
   client = Selenium::WebDriver::Remote::Http::Default.new
   client.timeout = 600
   $browser = Watir::Browser.new type, :http_client => client
   $timeout_length = 30 
  end
end

def close_browser
 # if there's a browser instance open, close it
 $browser.close if not $browser == nil
end 

Let’s also put in a couple of pieces of code (called “hooks”) that Cucumber knows to execute at certain times–once before a scenario and once after everything’s done:

Before do |scenario| 
 initialize_browser
end

After do 
 close_browser
end

Then we can start filling out steps. Start with navigating to the url:

Given(/^I go to "([^"]*)"$/) do |arg1|
 go_to_url(arg1)
end

Then clicking the Flights link:

When(/^I click the Flights link$/) do
 $browser.element(:xpath => "//a[contains(.,'Flights')]").when_present.click
end

And finally checking to see the flights have the correct price:

Then(/^the flight from "([^"]*)" to "([^"]*)" should cost "([^"]*)"$/) do |arg1, arg2, arg3|
	price = $browser.element(:xpath => "//tr//font[contains(text(), '#{arg1}') and contains(text(), '#{arg2}')]/../../td[2]//font").text
	fail "Price was wrong for flight from #{arg1} to #{arg2}--expected #{arg3} but got #{price}" if price != arg3
end

Run the test from the directory above features/ by typing ‘cucumber’ and you should (hopefully!) see these tests pass.

Refactoring

The next step is to do a little refactoring. This is the part where the power of any framework comes into play.

Any change you make to your code at this point should improve any one of: readability, maintainability or transferability. There are probably others, but I’m gonna stick with these as the main ones, because it seems like most any change fits into these three broad categories.

Refactoring for Maintainability

Let’s take a look at one of the steps:

When(/^I click the Flights link$/) do
 $browser.element(:xpath => "//a[contains(.,'Flights')]").when_present.click
end

If you do an Inspect Element on any of the links on the page, you’ll notice there’s a pattern: the text of the link itself can be used to locate the link itself.

If you needed to have a step for any or all of those links, you could write an individual step for each of them. But instead let’s refactor for maintainability:

When(/^I click the (.*) link$/) do |link|
 $browser.element(:xpath => "//a[contains(.,'#{link}')]").when_present.click
end

Now we have a single step that will work for any link. Check it out:

Feature: New Tours Flights Page Tests

Scenario: Check Flights and Prices
Given I go to "http://newtours.demoaut.com/index.php"
When I click the Home link
When I click the Flights link
When I click the Hotels link
When I click the Car Rentals link
When I click the Cruises link
When I click the Destinations link
When I click the Vacations link
...

Refactoring for Transferability

Another refactoring is one that helps with how others can use the steps you’ve written.

This is a pretty simple example. Likely you can open the step definitions file and see the step you want right on the screen without much scrolling.

But as you and your team(s) make more steps, it’s easy to have steps get lost deep in the bowels of a file somewhere.

A way to mitigate this is write the steps in a way that no matter how people might specify it, they’ll hit that step.

Consider the above step where a link is clicked. You might say “When I click the Flights link” but someone else might say, “When I click on the Flights link”. A change like this shouldn’t cause a new step to be written.

So let me show you a couple ways we can do a transferability refactoring:

When(/^I click (?:the|on the) (.*) link$/) do |link|
 $browser.element(:xpath => "//a[contains(.,'#{link}')]").when_present.click
end

or

the = "(?:the|on the)"
When(/^I click #{the} (.*) link$/) do |link|
 $browser.element(:xpath => "//a[contains(.,'#{link}')]").when_present.click
end

For both of these examples, we’re using a non-capturing regex to say that the string will match either “the” or “on the”. We use non-capturing because we don’t need to use that pattern match for anything inside the code, like we do for the name of the link we want to click.

This kind of refactoring makes your code more transferable to other people–there’s no “tribal knowledge” associated with knowing how to use this, as in, “Was it ‘I click on the Flights link’ or was it ‘I click the Flights link’?” It’s both. Either will work. Now you can spend less time trying to remember incantations, and more time thinking about what you’re trying to test.

Refactoring for Readability

So the step definition currently looks like this:

When(/^I click #{the} (.*) link$/) do |link|
 $browser.element(:xpath => "//a[contains(.,'#{link}')]").when_present.click
end

Instead of the big $browser line, we could shrink it down to something more readable, like:

When(/^I click #{the} (.*) link$/) do |link|
 HomePage::click(link)
end

This is where a page object comes in.

Page objects are pretty standard in automated testing. They’re generally used to describe stuff about what’s going to be on a particular page, and help organize your code a bit.

Let’s make one. Recall from running “cucumber –init” before, that a support directory was created. Anything that you put in this directory will get pulled in and required at runtime.

Start by creating a page_objects directory under that one, and create a file in there called HomePage.rb:

using support directory for holding page objects

Then the contents of HomePage.rb will be:

class HomePage
 
   @locators = {
      'Flights' => '//a[text()="Flights"]',
   }
 
   def self.click(element)
      $browser.element(:xpath => @locators[element]).when_present.click
   end
end

Now, when you want to click the Flights link, you can just call HomePage::click(“Flights”).

Actually… this not only enhances readability, but maintainability too, because you can have your locators all in one place instead of in multiple places in your steps.rb file. Bonus!

Conclusion

This wraps up Part 2. I hope you’ve been able to learn some things and pick up some tricks that you can apply.

I’ve got some other things I want to write about, but with the next part, I’ll show you some more tricks that allow you to do things like:

  • Automatically install and require any gems that are needed for running tests,
  • Take screenshots when tests fail,
  • Use a generic Page Object so you don’t have to remember locators from Page Objects without having to remember what Page Object it was in (i.e.: what happens if you have a “When I click the Submit button”, but 2 pages have Submit buttons, and they both have different locators? Intriguing…),
  • Make… shoot, I forgot what I was going to say here.
  • And maybe some other stuff I can’t think of right now.

Stay tuned for Part 3 and thanks for reading!

Fritz


Howdies. If you’re new here, I’d like to introduce myself.

I’m Fritz, and I’m a test-a-holic. I’m also an automation-automation enthusiast. 

“What’s automation-automation?” I hear you ask. And that’s a great question. 

Automation by itself is not a complete solution. To get the full benefits of it, you need a good pipeline that runs your automation for you on schedule, so that you get the fast feedback required for your team and company to shove out awesome product quicker, and have less risk of bonky software causing bugs that you have to fix later when you really wanted to work on something cool, amirite? 

The cool kids call what I’m describing, “DevOps”, and I like it so much that I started up a consultancy called Arch DevOps, where I provide services specifically geared for this stuff.

BOOM! Shameless plug! Just snuck it right up on ya.

If you enjoy this blog, please consider following me by clicking the button which at the time of this writing is along the right hand side and says “CLICKETY”. Anytime a new post is written, you’ll get an email as soon as it’s published.

That’s right, I automated the F5 key just for you. 

Thank you tons for your readership, and I look forward to sharing more with you!

Fritz

 

2 thoughts on “Writing a UI Automation Framework – Part 2

  1. Hi there. Your example is really good.
    I like it how you build up from the basics and go to refactoring and foreseeing ways the code can be kept manageable.
    The only point i would suggest is that when you say “Kick off a run by typing “cucumber” in the framework directory” you give an example. I.e. “Kick off a run by typing “cucumber” in the framework directory, e.g. Testzius Framework\cucumber features\flights.feature”
    keep up the good job!!!

    Like

  2. Thanks for the tutorial. Trying to follow along the steps under “Giving It Color”. Here are the issues I encountered:

    1) Following the steps as described above, I ended-up with an extra “end” in the script.
    –> Deleting the last “end”, moved me along.

    2) C:/Ruby22/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:128:in `require’: `require “watir-webdriver”` is deprecated. Please, use `require “watir”`.
    –> Replacing with “require watir” declaration on top of the script fixed the error

    3) Selenium::WebDriver::Remote::Http::Default#timeout= is deprecated. Use #read_timeout= or #open_timeout= instead

    4) Unable to find Mozilla geckodriver. Please download the server from https://github.com/mozilla/geckodriver/releases and place it somewhere on your PATH. More info at https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver.
    (Selenium::WebDriver::Error::WebDriverError)
    –> gem install selenium-webdriver
    –> download geckodriver, copy to C:\Windows and ensure it is added to PATH variable

    5) This leads to a new error, currently stuck at this:
    Scenario: Check Flights and Prices # features/flights.feature:3
    Permission denied – bind(2) for “::1” port 4444 (Errno::EACCES)

    Like

Leave a comment