rSmart's TestFactory Gem
Overview
This gem contains the basic framework for dryly creating test scripts for the web site that needs testing.
Use it to abstract away from the underlying Watir code and create your own DSL.
With TestFactory you have the ability to...
- Easily instantiate page classes (described below) in a consistent and readable manner
- Concisely describe elements on a page, keeping it DRY by avoiding repetition of element
identifiers that may (will) change
- Provide higher-level methods that use customizable (and default) data, along with the
page classes and elements, to perform user-oriented--i.e., behavioral--functions
with minimal lines of code
Tremendous thanks is due to Alister Scott, whose custom page object code for the Wikimedia Foundation provided the inspiration for this gem.
Summary
Using the TestFactory properly involves three distinct steps:
- Creating page classes that contain references to the elements on your web page. For this
you use the PageFactory class. Working on page classes requires that you have a strong
command of Watir and basic skills with Ruby.
- Creating "data objects" that utilize your page classes and elements to build methods that
perform user-oriented tasks. For this you use the DataFactory module. Working on data
objects requires you have good familiarity with Watir and strong Ruby skills.
- Creating test scenarios using your favorite test framework (like Cucumber or Rspec) and
your data objects. The methods in the Foundry class are useful here. Working at this
level requires only basic skills with Ruby and Watir, but a strong command of your DSL
(the thing you're building with TestFactory).
These three steps can all be accomplished by a single person. However, ideally, they should be done by three or four people, as the design philosophy of TestFactory allows for specialization:
- A Watir specialist works on defining page elements and actions inside of page classes
- A Ruby specialist uses the output of the Watir specialist to build the data objects and their helper methods, creating the DSL for the project
- A non-programmer--say, a business analyst or a manual tester with domain expertise--writes test scenarios, in English
- A more junior automation engineer translates the English into Ruby code, via the DSL created by the Ruby specialist (if you're using Cucumber, these would be your step definitions)
How to Start
First install the gem, of course.
gem install test-factory
Now you'll want to start building your own page classes, using the methods in TestFactory as your tool chest.
Please note that the following example is very simplified and contrived, to keep every step as compartmentalized as possible. Once you've read through this, it is strongly recommended that you visit an actual repository that is using the test factory.
Here is one such.
Begin by creating a BasePage class. This class should have PageFactory as its superclass and define sets of class elements that are generally common across the pages of your site.
require 'test-factory'
class BasePage < PageFactory
class << self
def header_elements
element(:main_menu_link) { |b| b.link(title: "Main Menu") }
element(:logout) { |b| b.button(value: "Logout") }
element(:administration_link) { |b| b.link(title: "Administration") }
action(:main_menu) { |p| p.main_menu_link.click }
action(:provide_feedback) { |b| b.link(title: "Provide Feedback").click }
action(:administration) { |p| p.administration_link.click }
end
end
end
Next, you create classes for the individual pages in your web site. These classes should have BasePage as their superclass, and should declare any of the relevant methods defined in the BasePage class.
class Home < BasePage
header_elements
expected_element :title
element(:title) { |b| b.h3(id: "title") }
end
Once you've got a bunch of classes set up for your site's various pages, you're going to want to create "data objects" to represent what goes into those pages. For this, you'll use the superclass DataFactory. Your data classes should follow this basic structure:
class YourDataObject < DataFactory
attr_accessor :title, :id, :link, :status, :description
def initialize(browser, opts={})
@browser = browser
defaults = {
:title=>"My Data Title",
:description=>"My Data's Description"
}
set_options(defaults.merge(opts))
requires :id
end
def create
end
def view
end
def edit opts={}
update_options(opts)
end
def remove
end
end
Now you have your basic infrastructure in place, and you can start writing your test cases using these classes.
include Foundry
@my_thing = make YourDataObject :id=>"identifier", :description=>"It's lovely."
@my_thing.create
on MyPage do |page|
page.title.set "Bla bla"
page.description==@my_thing.description ? puts "Passed" : puts "Failed"
end
Design Pattern
The TestFactory was written assuming the following guiding principles. Any code that does not
follow them probably smells, and should be refactored.
- Page Classes contain methods relating to interactions with page elements only--meaning
the getting or setting of values, or the clicking of links or buttons. Any more
complicated page interactions are handled in the Data Object classes, or in the test
step definitions.
- Data Objects represent definable data structure entities in the system being tested.
As data, they fit into the CRUD Model
and thus have methods that correspond to those basic functions.
- Data Objects have a single method for each of the CRUD functions, and additional
custom methods are avoided, unless there are compelling arguments for their inclusion in the class.
- When a Data Object is executing its
edit
method, first the data in the
system under test is updated, then the data object's instance variables
are updated--using DataFactory's set_options
. - Site navigation is handled using conditional methods (meaning they only navigate if
necessary) inside the Data Object--and preferably inside the data object's CRUD methods
themselves--unless there are specific reasons to explicitly navigate in a step
definition. This keeps step definitions from being unnecessarily cluttered.
- Specifying non-default test variables for data objects is done using key/value hash
pairs that are parameters of the data object's CRUD methods. It is not
done by explicitly assigning values to the instance variables. Examples:
@data_object = make DataObject, :attrib1 => "Custom Value 1", :attrib2 => "Custom Value 2"
@data_object.edit attrib1: "Updated Value 1", attrib2: "Updated Value 2"
@data_object.attrib1="Another Value"
- Except in very rare cases, updates to a data object's instance variables should be handled only by the
set_options
method, not explicitly.
def edit opts={}
page.element.fit opts[:value]
update_options(opts)
end
def edit opts={}
page.element.fit opts[:value]
@value=opts[:value] unless @value==opts[:value]
end
- The setting of random values for select lists in a data object is determined by passing
the string '::random::' in the instance variable, or as the value in the key/value pair
passed in an
#edit
method's opts
parameter. The #create
and #edit
methods will
handle the necessary logic. The purpose is to prevent the need for custom randomizing
CRUD methods in the data object. - See the gem_ext.rb file's discussion of the Watir
#fit
method for additional
design pattern rules to follow (If you're reading this on rubydoc.info then click the Watir module link) - Please make an effort to follow the Ruby Style Guidelines.