A tale of three ruby automated testing APIs (redux)

Redux Note: I originally wrote a similar article to this before going on parental leave about six weeks ago. Whilst I didn’t intend to offend, it seemed that a few people took my article the wrong way. I understand that a lot of effort goes into creating a web testing API, but that doesn’t mean that everyone will agree with what you’ve made.

Sadly, an anonymous coward attacked myself and the company who I work (even though I don’t mention that company on this blog), so for the first time in this blog’s history, I have had to turn comment moderation on. I am sorry to the other genuine commenters whose comments have been lost in transition, and now have to wait for their new comments to be approved.

Since then I have received numerous emails asking where my article went, and commenting that people found it interesting and worthwhile. So I have decided to repost this article, hopefully with a little less contention this time around, making it clear, this is my opinion and experience: YMMV.

Intro

As a consultant I get to see and work on a lot of automated testing solutions using different automated web testing APIs. Lately I’ve been thinking about how these APIs are different and what makes them so.

My main interest is in ruby, and fortunately ruby has three solid examples of three different kinds of web testing APIs, two of which extend the lowest level API: selenium-webdriver.

I’ll (try to) explain here what I consider to be three kinds of automated web testing APIs and where I consider the sweet spot to be and and why.

A meaty example

As a carnivore, I thought I would explain my concept in terms I can relate to. If you’re a beef eater, there are many different kinds of beef that you can use to make some tasty food to eat. I’ll use three different kinds of beef for my example. The first (rawest) kind would involve getting a beef carcass and filleting it yourself to eventually make some edible food. The second kind of beef you could use is beef that is already in a slightly usable form, but you can then use yourself to make some edible food. For example, you can buy minced beef at a butcher, and then make your own hamburger patties, taco fillings etc from it. The final type of beef you could use is beef that has already been prepared so you can directly consume it, for example, sausages which can be cooked and consumed as is.

I consider these three examples of different kinds of beef to roughly correlate to automated web testing APIs, of which I also consider to be three kinds of.

The first is a Web Driver API, which is the rawest form of an API, its job is to drive a browser by issuing it commands. It provides a high level of user control, but like filleting a beef carcass it’s more ‘work’. An example in ruby of this API is the selenium-webdriver API, which controls the browser using the webdriver drivers.

The second kind of automated web testing API is the Browser API, which is a higher level API but still provides user control. This is the minced beef of APIs, as whilst it’s in a more usable form than a carcass, you still have a lot of control (and potential to what you can do with it). An example in ruby of this API is the watir-webdriver API, which uses the underlying selenium-webdriver carcass to control the browser.

The final kind of automated web testing API is the Web Form DSL (Domain Specific Language) which is a very high level API that provides users with specific methods to automate web forms and their elements. This is the beef sausages of APIs as sometimes you feel like eating something else besides sausages, but it’s difficult to make anything else edible but sausages from sausages. An example in ruby of this Web Form DSL is the Capybara DSL.

Visually, this looks something like this:

Show me the code™

So exactly what do these APIs look like?

I knew you’d ask, that’s why I came prepared.

Say I want to accomplish a fairly basic scenario on my example Google Doc form:

  • Start a browser
  • Navigate to the watir-webdriver-demo form
  • Check whether text field with id ‘entry_0′ exists (this should exist)
  • Check whether text field with id ‘entry_99′ exists (this shouldn’t exist)
  • Set a text field with id ‘entry_0′ to ’1′
  • Set a text field with id ‘entry_0′ to ’2′
  • Select ‘Ruby’ from select list with id ‘entry_1′
  • Click the Submit button

This is how I would do it in the three different APIs:

# * Start browser
# * Navigate to watir-webdriver-demo form
# * Check whether text field with id 'entry_0' exists
# * Check whether text field with id 'entry_99' exists
# * Set text field with id 'entry_0' to '1'
# * Set text field with id 'entry_0' to '2'
# * Select 'Ruby' from select list with id 'entry_1'
# * Click the Submit button

require 'bench'

benchmark 'selenium-webdriver' do
  require 'selenium-webdriver'

  driver = Selenium::WebDriver.for :firefox
  driver.navigate.to 'http://bit.ly/watir-webdriver-demo'
  begin
    driver.find_element(:id, 'entry_0')
  rescue Selenium::WebDriver::Error::NoSuchElementError
    # doesn't exist
  end
  begin
    driver.find_element(:id, 'entry_99').displayed?
  rescue Selenium::WebDriver::Error::NoSuchElementError
    # doesn't exist
  end
  driver.find_element(:id, 'entry_0').clear
  driver.find_element(:id, 'entry_0').send_keys '1'
  driver.find_element(:id, 'entry_0').clear
  driver.find_element(:id, 'entry_0').send_keys '2'
  driver.find_element(:id, 'entry_1').find_element(:tag_name => 'option', :value => 'Ruby').click
  driver.find_element(:name, 'submit').click
  driver.quit
end

benchmark 'watir-webdriver' do
  require 'watir-webdriver'
  b = Watir::Browser.start 'bit.ly/watir-webdriver-demo', :firefox
  b.text_field(:id => 'entry_0').exists?
  b.text_field(:id => 'entry_99').exists?
  b.text_field(:id => 'entry_0').set '1'
  b.text_field(:id => 'entry_0').set '2'
  b.select_list(:id => 'entry_1').select 'Ruby'
  b.button(:name => 'submit').click
  b.close
end

benchmark 'capybara' do
  require 'capybara'
  session = Capybara::Session.new(:selenium)
  session.visit('http://bit.ly/watir-webdriver-demo')
  session.has_field?('entry_0') # => true
  session.has_no_field?('entry_99') # => true
  session.fill_in('entry_0', :with => '1')
  session.fill_in('entry_0', :with => '2')
  session.select('Ruby', :from => 'entry_1')
  session.click_button 'Submit'
  session.driver.quit
end

run 10

This is how long they took for me to run:

                        user     system      total        real
selenium-webdriver  1.810000   0.840000  22.130000 ( 73.123340)
watir-webdriver     1.940000   0.870000  24.380000 ( 79.388494)
capybara            1.950000   0.890000  24.080000 ( 79.920051)

Note: Capybara doesn’t always require a ‘session’, it’s only for non ruby rack applications, but since my example (Google) is not a rack application, as are most of the applications I test, my example must use the session.

When using ruby, why Watir-WebDriver is my sweet spot

I personally find Watir-WebDriver to be the most elegant solution, as the API is high enough for me to be highly readable/usable, but low enough to be powerful and for me to feel like I’m in control.

For example, being able to select an element by a explicit identifier (name, class name, id, anything) is a huge deal to me. I personally don’t like relying on the API to determine which selector to use: for example Capybara only supports name, id and label, but you can’t tell fill_in which specific one to choose: it appears to try each selector one by one until it finds it.

I have found that Watir-WebDriver also also provides lots of flexibility/neatness. For example: it’s the only API shown here that allows URLs to not have a ‘http://’ prefix (how many people do you know who type in http:// into a browser?).

In my opinion, the high level APIs like Capybara don’t provide enough control (for example – being able to specify the explicit selector), but the low level APIs like webdriver don’t provide enough functionality. This is evident when I am using a language other than ruby (like C#) when I find myself writing a large number of web element extension methods because webdriver doesn’t provide any of them. A .set method is a classic example, even Simon Stewart writes a clearAndType method in his examples even though he wrote webdriver which sadly misses it (you must call .clear, and .send_keys).

My biggest concern about high level field APIs

But my biggest issue with the high level APIs is that I’ve frequently seen them used to write test scripts that are step by step interactions with a web form. Instead of thinking of a business application as that, people see it as a series of forms that you ‘fill in’. This means people create scenarios like Aslak Hellesøy included in his recent post about cucumber web steps (which uses Capybara) and the problems it has created.

Scenario: Successful login
  Given a user "Aslak" with password "xyz"
  And I am on the login page
  And I fill in "User name" with "Aslak"
  And I fill in "Password" with "xyz"
  When I press "Log in"
  Then I should see "Welcome, Aslak"

I’m not saying it’s not possible to end up with something as ugly as above using other APIs, but I am saying the web form DSL style naturally relates to this: as the APIs look so similar to this style because that’s what the DSL was designed for: filling in forms. I’ve seen people frequently write generic, reusable cucumber steps to match the web form DSL like:

When /^I fill in "(.+)" with "(.+)"$/ do |value, field|
  fill_in field, :with => value
end

But this means you end up with less readable, less maintainable test scripts rather than business readable executable specifications.

Summary

Ultimately what I am looking for in an automated web testing API is simplicity and full control. I personally find browser APIs like Watir-WebDriver and Watir give me this, and this is why I love them so. Your mileage may vary, you may like different styles of APIs better, but I’ve seen other APIs so badly abused by people not even thinking about it, so it makes sense to think about what you’re trying to achieve and whether what you’re doing is the right way.

Determining your host operating system using ruby

I found out this is how you programatically determine your host operating system using ruby:

require 'rbconfig'
RbConfig::CONFIG['host_os']

This came in handy when setting the automatic browser downloads for browsers on Windows, as the location needs to use backslashes on Windows instead of Ruby’s default forward-slashes:

require 'rbconfig'
download_directory = "#{Dir.pwd}/downloads"
download_directory.gsub!("/", "\\") if  RbConfig::CONFIG['host_os'] =~ /mswin|mingw/

Your automated acceptance tests needn’t be written in the same language as your system being tested

When selecting which tool to implement for business facing automated acceptance tests, I’ve often heard that it must use the same programming language as the system under test. Whilst this can sometimes work well, often it is better to choose a tool in another language that will ulimately deliver you better success as it can be adopted more passionately. Here are some of the reasons on why I think so.

The simplicity of some other languages mean it’s easier for less technical testers to understand them

Let me show you an example that compares the same test written in C# as in Ruby.

C# WebDriver Example – Inspired by David Burns

using OpenQA.Selenium;
using OpenQA.Selenium.IE;
 
using NUnit.Framework;
 
namespace Selenium.Two.DotNetExample
{
    [TestFixture]
    public class Test_Google
    {
	    IWebDriver driver;
	     
	    [SetUp]
	    public void Setup()
	    {
		    driver = new InternetExplorerDriver();
	    }
	    
	    [TearDown]
	    public void Teardown()
	    {
		    driver.Quit();
	    }
    		
        [Test]
        public void TestSearchGoogleForTheAutomatedTester()
	    {
	        //Navigate to the site
            driver.Navigate().GoToUrl("http://www.google.com");
            //Find the Element and create an object so we can use it
            IWebElement queryBox = driver.FindElement(By.Name("q"));
            //Work with the Element that's on the page
            queryBox.SendKeys("Watir");
			queryBox.SendKeys(Keys.ArrowDown);
            queryBox.Submit();
            //Check that the Title is what we are expecting
            Assert.True(driver.Title.IndexOf("Watir - Google Search") > -1);
        }
    }
}

Ruby Watir Example – written by me

require 'watir-webdriver' #watir, celerity, firewatir
require "test/unit"
class GoogleTest < Test::Unit::TestCase
  def test_google
    b =Watir::Browser.start "www.google.com"
    b.text_field(:name => "q").set "watir"
    b.button(:name, "btnG").click
    assert_equal("Watir - Google Search", b.title)
    b.close
  end
end

I am slightly biased, but I think the ruby example is neater and easier to read, especially to someone with a less technical background. Sure, a C# developer might baulk at this premise, but for most people who don’t know C#, I imagine it’s the case.

Often times developers like learning another programming language to add to their experience
When I’ve been working on automated tests in ruby, I’ve often seen a strong interest in developers wanting to learn ruby and the automated testing framework, as it’s common for developers to specialize in a language (Java or .NET) and then focus solely on that. Don’t get me wrong, this isn’t always the case, but it happens more than you think.

Testers can embrace simpler programming languages such as ruby, and can become the “experts” in that domain

I’ve seen testers pick up a programming language like ruby and polish their skills in it, which helps their morale by becoming experts in it. If the automated acceptance tests were written in the same language as the developers, yes this brings benefits, but the testers might not become experts as quickly, and may lack ownership of the tests.

Automated acceptance test tools provide different mileage across languages
Some languages have particularly good tools for defining business facing automated tests, for example, Cucumber’s support for ruby is superb, whereas support for C# equivalents such as Cuke4Nuke and SpecFlow isn’t quite as good. RSpec is another example of a tool that is awesome in its domain.

Summary

I know this post has ended up sounding like a pro-ruby rant, but it is :) I am trying to point out that there’s simpler, easier languages for testers to pick up than your Javas and C-Sharps. The ruby and python programming languages are two great examples, and as I mentioned, it seems that ruby is quickly becoming the testing language of choice, because of its simplicity, and the wide variety of testing related gems available. Ruby also makes it extremely easy to bootstrap configurations across different environments, and its lack of licensing requirements or need for an IDE make it an excellent choice for testers.

One of the reasons that the Selenium project is so successful is that it supports various programming languages (and browsers), which appeals to the majority of developers who can use their established skills, but it’s common to meet testers who are passionate about Watir, because of its simplicity and focus: a tool for testers by testers. It is for this reason I believe Ruby & Watir & Cucumber is a winning combination, and will bring about many success stories for agile testing teams in the future.

Reducing Cucumber page object element duplication using mixins

The one thing that quickly happens when you start using a page object pattern to develop your Cucumber acceptance tests is you end up with duplicate page elements in multiple page objects. This happens because usually your pages often have elements common to every page and it doesn’t make sense defining these more than once, enter page mixins.

Page mixins are methods in ruby that you can mix into a class, so that each method in your mixin is then available in the instance of your class. We can use a mixin to define our common elements which are then available for every class we define.

For example, in our Google/Bing search example, the search text fields are conveniently given the same id in each of the search engine pages, therefore it makes sense to define this once and mix it in to each page.

We create a mixin file which is essentially a module with a bunch of methods. In our case, we define initialize to define our elements, and method missing as this is a common method we use for Browser object delegation.

module PageMixIn
  attr_accessor :search_field

  def initialize *args, &block
    @search_field = self.text_field(:name => "q")
  end

  def method_missing sym, *args, &block
    @browser.send sym, *args, &block
  end
end

We then simply include this mixin into our page class, and the methods are mixed in. The super statement in the class’s initialize method is needed to ensure the instance variables of the mixin are available in the class.

class GoogleHomePage
  include PageMixIn

  attr_accessor :search_field, :google_search_button

  URLS = { :production => "http://www.google.com/" }

  def initialize(browser)
    @browser = browser
    @google_search_button = @browser.button(:name => "btnG")
    super
  end

  def visit
    @browser.goto URLS[:production]
  end

  def search_for term
    self.search_field.set term
    self.google_search_button.click
    google_results_page = GoogleResultsPage.new(@browser)
    google_results_page.results.wait_until_present if WEBDRIVER
    google_results_page
  end
end

This means we eliminate a lot of duplication of page elements, such as the search field, as well as common methods such as the method_missing.

Summary and a quick tip

I have found using page mixins provides a flexible approach to reducing page object element and method duplication. It makes your pages easy to read and compact. A quick tip though: I thoroughly recommend storing all page objects and page mixins under the support directory in Cucumber, as this ensures they are loaded automatically by Cucumber and are consistently available to reference.

Composition or inheritance for delegating page methods?

The thing I like to do when creating a page object pattern for automated web testing is delegating any methods that don’t belong to the Page object itself.

For example, a very simple page object model like this GoogleHomePage doesn’t delegate any methods to the Browser object.

require "rubygems"
require "watir-webdriver"

class GoogleHomePage
  def initialize(browser)
    @browser = browser
  end
  def visit
    @browser.goto "www.google.com"
  end
end

b = Watir::Browser.new :firefox
p = GoogleHomePage.new b
p.visit
puts p.title
p.close

So, the p.title and p.close statements both fail with an error: undefined method `goto' for # (NoMethodError).

One approach would be simply to write appropriate methods for what you would do on the Browser object. For example:

require "rubygems"
require "watir-webdriver"

class GoogleHomePage
  def initialize(browser)
    @browser = browser
  end
  def visit
    @browser.goto "www.google.com"
  end
  def title
    @browser.title
  end
  def close
    @browser.close
  end
end

b = Watir::Browser.new :firefox
p = GoogleHomePage.new b
p.visit
puts p.title
p.close

But this isn’t DRY. It means every method of Browser you access to needs to be rewritten. But I often see this happen.

What we should be doing is simply delegating any methods that don’t exist on the Page object to the Browser object which is passed in at initialization. There are two ways I know of to do this: inheritance delegation and composition.

Inheritance Delegation

Inheritance delegation means changing our class so it delegates appropriately using a DelegateClass. This means anything of class Browser is delegated.

For example:

require "rubygems"
require "watir-webdriver"

class GoogleHomePage < DelegateClass(Watir::Browser) 
  def initialize(browser)
    super(browser)
  end
  def visit
    self.goto "www.google.com"
  end
end

b = Watir::Browser.new :firefox
p = GoogleHomePage.new b
p.visit
puts p.title
p.close

From this point forward you don’t need to refer to @browser, instead you just refer to self in your class.

Composition

Composition is about composing the class of different elements, some of which are passed to the browser. This essentially involves creating a method_missing method and passing these methods to the instance variable @browser.

require "rubygems"
require "watir-webdriver"

class GoogleHomePage
  def initialize(browser)
    @browser = browser
  end
  def method_missing(sym, *args, &block)
    @browser.send sym, *args, &block
  end
  def visit
    @browser.goto "www.google.com"
  end
end

b = Watir::Browser.new :firefox
p = GoogleHomePage.new b
p.visit
puts p.title
p.close

This means that any reference to the Browser object still needs to refer to @browser throughout the class.

Inheritance or Composition?

You can see from the examples above, both approaches are very similar, but from researching these, it seems that most people prefer composition to inheritance in ruby, mainly due to maintainability of class chains. In our example, the inheritance chain is small and very simple, so I don’t think this poses a great maintainability issue.

What do you think? What would you prefer?

Easily manage your rubies with RVM, Bundler and Pik

Background

I’ve spent some time working on setting up an acceptance test framework using Cucumber and ruby under OSX on my ThoughtWorks MacBook Pro as my dev machine.

As a consultant, you can often work on a variety of projects, and these often have different ruby version requirements and use different ruby gems and dependencies. If you don’t use the help of a tool, it quickly becomes difficult to manage these various ruby versions, gems and dependencies.

RVM to the rescue

If you’re using Mac OSX and *-nix platforms, Ruby Version Manager, or RVM, is the tool for you:

“RVM is a command line tool which allows us to easily install, manage and work with multiple ruby environments from interpreters to sets of gems.”

At first, RVM was quite confusing to me, but over a short period of time I have grown to love it. It is *-nix only, but later I will explain what exists in the Windows world that is equivalent (Pik).

Reasons why RVM rocks

  • You can isolate and manage rubies and gemsets in those rubies
  • You associate these rubies and gemsets to projects – using .rvmrc and Gemfile files – so they consistently use the same ruby and gemset
  • It works with Bundler to fetch your ruby gems

RVM Explained in a Diagram

I decided to draw a diagram to explain this, using the really awesome Google docs drawings (goodbye Visio license costs).

RVMExplainedDiagram-AlisterScott

RVM Explained: click for larger view

.rvmrc files

You create one of these in your project root, which specifies which ruby version, and gemset, you want the project to use. When you “cd” into this directory, this is triggered by RVM to do the magic of switching your ruby version – so you’re system thinks you’re running a different version (the first time it asks you if you would like to trust it).

An example .rvmrc file


rvm_install_on_use_flag=1
rvm --create use jruby-1.5.3@celerity

An example message when cd’ing into this dictory


Using /Users/alisterscott/.rvm/gems/jruby-1.5.3 with gemset celerity

The really, really neat thing about this is, if you don’t have that version of ruby, or JRuby, installed then it will automatically fetch it and install it. Neat!

Managing your gems within RVM using Bundler

The second component of RVM that is very neat is gemsets.

A gemset is a collection of gems, that lives in a container that is associated with a version of ruby you have created in RVM: so you can have the same ruby version with various collections or groupings of gems.

Bundler (a gem itself) is used to collect your gems from your specified source – usually rubygems.org, but this can be a local gemserver if set up one in a corporate environment due to proxy issues.

As you’re running RVM with a ruby version and gemset already specified, when you run bundler (bundle install), these gems are inserted into your Gemset container. If you then switch RVMs or gemsets, you would simply rerun “bundle install” to insert the same gems into the different container. This is essentially what happens the first time you run your project on a server, the RVM or the Gemset won’t exist, so you create a script to switch to your correct rvm (using the .rvmrc file), install the bundler gem (because you need this to get the gem bundle), then run a bundle install.

Using RVM and Bundler to bootstrap configuration for running Cucumber features under C.I.

Use can RVM and Bundler to ensure your continous integration server runs your cucumber features correctly. An example shell script to do this on a unix box would be:


rvm rvmrc trust
cd features
cd .. # hack to load RVMRC
set -e
gem install bundler --no-rdoc --no-ri
bundle install
cucumber -p ci

A typical Gemfile


source "http://rubygems.org"
gem "cucumber", "0.10.0"
gem "prawn", "0.8.4"
gem "watir-webdriver", "0.1.8"
gem "celerity", "0.8.7"

I find it is best to specify explicit versions of gems in the GemFile, so you don’t have unexpected  consequences of automatically updating to a newer version of a gem without you knowing.

Gemfile.lock file

When you run bundle install for the first time, a Gemfile.lock directory is created. This contains the entire set of gem dependencies and their known versions that aren’t explicitly specified in your Gemfile (dependencies of your dependencies). This file is important as without it, when you run bundle install, bundler will look for fresh gem depencies, and these could change which could cause all kinds of versioning weirdness. This is why I would encourage people to store this file under version control.

Ruby Versioning on Windows: use Pik

When researching RVM for windows, I came across Pik, a similar concept, but with a little less finesse. Pik lets you have multiple ruby installs on your windows machine, and quickly switch between, or pik, which one you want to use.


C:\>pik switch 191 p129
   == Switching to ruby 1.9.1p129 (2009-05-12 revision 23412) [i386-mingw32] ==

It’s not as cool in the sense it doesn’t get and install your rubies for you, and there doesn’t seem to be same gemset concept, but fortunately Bundler still works the same, and can read your Gemfile.

So the get your ruby environment running on Windows, you could do something like

  1. pik switch 1.8.7
  2. gem install bundler
  3. bundle install

Not quite as neat as RVM, but still pretty neat.

A final note: running Cucumber under JRuby on Windows using Pik

To get coloured output (or non-weird looking output) for Cucumber on Windows under JRuby, you need to use Wac.

  • Download wac and put it on your path (I usually create a c:\bin directory and put this on my path, and put things it in)
  • Run “cucumber | wac” to see proper Cucumber output

Summary

  • RVM is a super neat tool that makes managing multiple ruby versions and gemsets a breeze.
  • There are so many useful scenarios for it, it is hard to describe here, but they include testing, continuous integration and deployment.
  • I find checking in the .rvmrc, Gemfile and Gemfile.lock files into your project ensures consistent reproducible ruby environments across multiple machines.

Rubular & RubyMine: makes Cucumber easier

I’ve spent the last couple of months establishing Acceptance Test Driven Development on a medium sized software delivery project using Watir & Cucumber.

Cucumber is premised on reading feature files and matching strings to determine what to do, which is done by using regular expressions in step files. This means constant use of ruby regular expressions, and in polishing up on my regular expression skills, I have found rubular incredidly useful. I love the simple layout and the ‘Regex Quick Reference’ at the bottom, just where you need it. It’s very well done.

Rubular: A Ruby regular expression editor and tester

Rubular: A Ruby regular expression editor and tester

The other tool I have found incredibly useful is RubyMine. Up until now, I’ve never really bothered with an IDE for Watir stuff. I’ve mainly stuck to text editors before, but since I have started using Cucumber, I have found the RubyMine tool almost critical, as its support for Cucumber feature files and step definitions is superb. It features click through linking for feature files so you know exactly what step you’re calling, and the debugger is awesome; no more puts statements for me :)

So, if you’re thinking about implementing Cucumber, or using Cucumber but are annoyed with the lack of efficiency in managing a large suite of step definitions, I would thoroughly recommend these two tools. Rubular is free, and I believe RubyMine is about $99, but less if you need multiple licenses. There’s also an Early Access Program where you can use RubyMine for free as long as you’re happy to test it along the way.

Dynamically calling ruby methods in modules

When I am creating Watir tests, I write ruby methods to define user tasks, for example, adding a book to a cart becomes def add_book. I then group these ruby methods into ruby modules divided logically by the area of the application I am writing tests for. For example, I would have a ‘Customer’ module and an ‘Admin’ module for the Depot app. The benefit of using modules is you can avoid namespace conflicts as essentially each method is defined by its module’s prefix. This means that you can happily have def Customer.log_on and def Admin.log_on without any conflict or confusion.

As I have mentioned before, I like defining tests outside my code. These tests ultimately need to execute an associatted ruby method (stored in a module) by passing some data in (and getting an outcome and some output back). One way of calling these tests defined external to our code is to have a massive case statement that determines what calls what. This isn’t ideal as it is a maintenance burden, and really it isn’t needed.

In ruby it’s straightforward to dynamically load ruby modules, and then dynamically call individual methods.

require 'temp'

module_name = "Temp"
method_name = "hello_world"

required_module = Kernel.const_get(module_name)
required_method = required_module.method(method_name)
required_method.call('Alister')

This is all well and good if Temp.helloworld() exists, but if it doesn’t, our code throws exceptions:


`const_get': uninitialized constant Kernel::Temp (NameError)

or

`method': undefined method `hello_world' for class `Module' (NameError)

One way to avoid these exceptions is to wrap the code with a rescue clause, but I realised there are some easy ways to check if both modules and methods exist before loading them.

require 'temp'

module_name = "Temp"
method_name = "hello_world"

if Object.const_defined?(module_name)
  required_module = Kernel.const_get(module_name)
  if required_module.respond_to?(method_name) then
    required_method = required_module.method(method_name)
    required_method.call('Alister')
  else
    puts "Invalid method '#{method_name}' for module '#{module_name}'"
  end  
else
 puts "Invalid module '#{module_name}'"
end

This ensures that the code continues to execute if the module or method name is specified incorrectly, which is sometimes the case if its specified in a spreadsheet, and especially if someone else has designed the spreadsheet.

Once we are happy about dynamically finding methods in modules, the next step is to make sure that each method is called with the correct number of parameters. This property of a method is called the arity.

The great thing about ruby and arity is that you simply determine the number of parameters and then pass in a correct sized array, using a *, and the receiving method will automatically unpack the array into the parameters specified.

puts required_method.arity()
required_method.call(*parameters)

The flexibility that ruby offers is amazing. I have tried to accomplish this same concept in VBScript but I couldn’t work out how. That’s why I am glad Watir uses ruby, it ultimately means my automated test framework is more efficient and maintainable.

Creating a Watir framework using Test::Unit & Roo

One common challenge I see over and over again is people figuring out how to design a logical and maintainable automated testing framework. I have designed quite a few frameworks for various projects, but one thing that has consistently been a win for me is purposely separating test case and test execution design.

It’s therefore logical that the design of my Watir framework deliberately separates test case design and test execution design so that:

  • test case design is done visually in spreadsheets; and
  • test execution design is done in ruby methods, because code is the most efficient and maintainable way.

Since I last published details about my framework on this blog, I have started doing assertions using the Test::Unit ruby library. The reasons I chose Test::Unit are:

  • it is easy to ‘mix-in’ Test::Unit assertions into modules of ruby code using include Test::Unit::Assertions;
  • it is included with ruby;
  • ruby scripts with Test::Unit::TestCase are instantly executable, in my case, from SciTE;
  • its assertions are easy to understand and use.

I have also made some other improvements to my framework code, including:

  • the ability to specify browser types, and spreadsheet sources, as command line arguments (with defaults);
  • logging test output to a file;
  • no longer attaching to an open browser, the same browser instance is used completely for all tests (and elegantly closed at the end).

The main design has been kept the same, in that a spreadsheet (either excel, openoffice or Google Docs) contains tests grouped by functional area, which call a method in a particular module.

The great thing about my framework is that adding a new test is a matter of designing the test case, and then writing the ruby method: as the methods are called dynamically from the spreadsheet, no extra glue is needed!

Enough talk, here’s the code. The Google spreadsheet is here. You can find a .zip file of all the required files to run it here. It runs on the depot app, which you get here. You will need two gems: Watir (oh duh), and Roo.

Test Driver tc_main.rb


$:.unshift File.join(File.dirname(__FILE__), ".", "lib")
require 'watir'
require 'roo'
require 'test/unit'
require 'customer'
require 'admin'
$stdout = File.new('log.txt',File::WRONLY|File::APPEND|File::CREAT)
$stderr = File.new('log.txt',File::WRONLY|File::APPEND|File::CREAT)

class TC_WatirMelon < Test::Unit::TestCase
  @@colmap = {:module_name=>0, :method_name=>1, :comments=>2, :exp_outcome=>3, :exp_error=>4, :first_param=>5}
  @@ss_format = ARGV[0]
  @@specified_browser = ARGV[1]

  def setup
    puts "[Starting at #{Time.now}]\n"
    case @@ss_format
      when "excel"
        @ss = Excel.new("watirmelon.xls")
      when "wiki"
        @ss = Excel.new("http://localhost:8080/download/attachments/2097153/watirmelon.xls")
      when "gdocs"
        @ss = Google.new("0AtL3mPY2rEqmdEY3XzRqUlZKSmM5Z3EtM21UdFdqb1E")
      else
        @ss = Openoffice.new("watirmelon.ods")
      end
    @ss.default_sheet = @ss.sheets.first
    case @@specified_browser
      when "firefox"
        Watir::Browser.default = 'firefox'
        @browser = Watir::Browser.new
      else
        Watir::Browser.default = 'ie'
        @browser = Watir::Browser.new
        @browser.speed = :zippy
        @browser.visible = true
      end
  end

  def test_run_sheet()
    @ss.first_row.upto(@ss.last_row) do |row|
      #Read row into array
      line = Array.new
      @ss.first_column.upto(@ss.last_column) do |column|
        line << @ss.cell(row, column).to_s.strip
      end

      module_name = line[@@colmap[:module_name]]
      if module_name != "Function" then #if not a header
        method_name = line[@@colmap[:method_name]].downcase.gsub(' ','_') #automatically determine ruby method name based upon data sheet
        exp_outcome = line[@@colmap[:exp_outcome]]
        exp_error = line[@@colmap[:exp_error]]
        first_param = @@colmap[:first_param]
        required_module = Kernel.const_get(module_name)
        required_method = required_module.method(method_name)
        arity = required_method.arity() # this is how many arguments the method requires, it is negative if a 'catch all' is supplied.
        arity = ((arity * -1) - 1) if arity < 0 # arity is negative when there is a 'catch all'
        arity = arity-1 # Ignore the first browser parameter
        unless arity == 0
          parameters = line[first_param..first_param+(arity-1)]
        else
          parameters = []
        end
        begin
          act_outcome, act_output = required_method.call(@browser, *parameters)
        rescue Test::Unit::AssertionFailedError => e
          self.send(:add_failure, e.message, e.backtrace)
          act_outcome = false
          act_output = e.message
        end
        if (exp_outcome == 'Success') and act_outcome then
          assert(true, "Expected outcome and actual outcome are the same")
          result = 'PASS'
        elsif (exp_outcome == 'Error') and (not act_outcome) and (exp_error.strip! == act_output.strip!)
          assert(true, "Expected outcome and actual outcome are the same, and error messages match")
          result = 'PASS'
        else
          result = 'FAIL'
          begin
            assert(false,"Row: #{row}: Expected outcome and actual outcome for #{method_name} for #{module_name} do not match, or error messages do not match.")
          rescue Test::Unit::AssertionFailedError => e
            self.send(:add_failure, e.message, e.backtrace)
          end
        end
        puts "###########################################"
        puts "[Running: #{module_name}.#{method_name}]"
        puts "[Expected Outcome: #{exp_outcome}]"
        puts "[Expected Error: #{exp_error}]"
        puts "[Actual Outcome: Success]" if act_outcome
        puts "[Actual Outcome: Error]" if not act_outcome
        puts "[Actual Output: #{act_output}]"
        puts "[RESULT: #{result}]"
        puts "###########################################"
        end
      end
  end

  def teardown
    @browser.close
    puts "[Finishing at #{Time.now}]\n\n"
  end

end

Customer Module customer.rb

require 'test/unit'
include Test::Unit::Assertions

module Customer

  TITLE = 'Pragprog Books Online Store'
  URL = 'http://localhost:3000/store/'

  # Description:: Adds a book named 'book_title' to cart
  def Customer.add_book(browser, book_title)
    browser.goto(URL)
    # Check if title is already in cart - so we can check it was added correctly
    browser.link(:text,'Show my cart').click
    prev_cart_count = 0
    prev_cart_total = 0.00
    if not browser.div(:text,'Your cart is currently empty').exist? then
     # We have a non-empty cart
      for row in browser.table(:index,1)
        if row[2].text == book_title then
          prev_cart_count = row[1].text.to_i
          break
        end
      end
      prev_cart_total = browser.cell(:id, 'totalcell').text[1..-1].to_f #remove $ sign
      browser.link(:text, 'Continue shopping').click
    end

    found = false
    book_price = 0.00
    1.upto(browser.divs.length) do |index|
      if (browser.div(:index,index).attribute_value('className') == 'catalogentry') and (browser.div(:index,index).h3(:text,book_title).exists?) then
        book_price = browser.div(:index,index).span(:class, 'catalogprice').text[1..-1].to_f #remove $ sign
        browser.div(:index,index).link(:class,'addtocart').click
        found = true
        break
      end
    end
    if not found then
      return false,'Could not locate title in store'
    end

    new_cart_count = 0
    for row in browser.table(:index,1)
      if row[2].text == book_title then
        new_cart_count = row[1].text.to_i
        break
      end
    end
    new_cart_total = browser.cell(:id, 'totalcell').text[1..-1].to_f # remove $ sign
    assert_equal(new_cart_count,(prev_cart_count+1), "Ensure that new quantity is now one greater than previously")
    assert_equal(new_cart_total,(prev_cart_total + book_price), "Ensure that new cart total is old cart total plus book price")
    browser.link(:text, 'Continue shopping').click
    return true,new_cart_total
  end

  def Customer.check_out(browser, customerName, customerEmail, customerAddress, customerPaymentMethod)
    browser.goto(URL)
    browser.link(:text,'Show my cart').click
    if browser.div(:text,'Your cart is currently empty').exist? then
      return false,'Your cart is currently empty'
    end
    browser.link(:text,"Checkout").click
    browser.text_field(:id, 'order_name').set(customerName)
    browser.text_field(:id, 'order_email').set(customerEmail)
    browser.text_field(:id, 'order_address').set(customerAddress)
    begin
      browser.select_list(:id, 'order_pay_type').select(customerPaymentMethod)
    rescue Watir::Exception::NoValueFoundException
      flunk('Could not locate customer payment method in drop down list: '+customerPaymentMethod)
    end
    browser.button(:name, 'commit').click
    if browser.div(:id,'errorExplanation').exist? then
      error = ''
      1.upto(browser.div(:id,'errorExplanation').lis.length) do |index|
        error << (browser.div(:id,'errorExplanation').li(:index,index).text + ",")
      end
      browser.link(:text,'Continue shopping').click
      return false, error
    end
    assert_equal(browser.div(:id,'notice').text, 'Thank you for your order.',"Thank you for your order should appear.")
    return true,''
  end

  def Customer.empty_cart(browser)
    browser.goto(URL)
    browser.link(:text,"Show my cart").click
    if browser.div(:text,"Your cart is currently empty").exist? then
      assert('Cart was never empty')
    else
      browser.link(:text,'Empty cart').click
      assert_equal(browser.div(:id, 'notice').text,'Your cart is now empty')
    end
    return true,''
  end

  def Customer.check_cart_total(browser, exp_total)
    browser.goto(URL)
    browser.link(:text,'Show my cart').click
    if browser.div(:text,'Your cart is currently empty').exist? then
      return false,'Your cart is currently empty'
    end
    act_total = browser.cell(:id, 'totalcell').text[1..-1].to_f
    assert_equal(act_total,exp_total.to_f,"Check that cart total is as expected.")
    return true,act_total
  end
end

Admin Module admin.rb


require 'test/unit'
include Test::Unit::Assertions

module Admin
  TITLE = 'ADMINISTER Pragprog Books Online Store'
  URL = 'http://localhost:3000/admin/'

  def Admin.log_on(browser, username, password)
    browser.goto(URL)
    if browser.link(:text,'Log out').exist? then #if already logged in
      browser.link(:text,'Log out').click
    end
    browser.text_field(:id, 'user_name').set username
    browser.text_field(:id, 'user_password').set password
    browser.button(:value, ' LOGIN ').click
    if browser.div(:id, 'notice').exist? then
      return false,browser.div(:id, 'notice').text
    else
      return true,''
    end
  end

  def Admin.ship_items(browser, name)
    browser.goto(URL)
    browser.link(:text, 'Shipping').click
    num_orders = 0
    index = 0
    browser.form(:action,'/admin/ship').divs.each do |div|
      if div.class_name == "olname"
        index+=1
        if div.text == name then
          browser.form(:action,'/admin/ship').checkbox(:index, index).set
          num_orders+=1
        end
      end
    end

    browser.button(:value, ' SHIP CHECKED ITEMS ').click

    if num_orders == 1 then
      assert_equal(browser.div(:id,"notice").text, "One order marked as shipped","Correct notice")
    elsif num_orders > 1 then
      assert_equal(browser.div(:id,"notice").text, "#{num_orders} orders marked as shipped","Correct notice")
    end
    return true, num_orders.to_s
  end

end