Just Give it a List!
Collection-driven, commandless Selenium
HoneyDo is a small Ruby/Javascript library that supports collection-driven browser automation. Currently implemented as a Selenium RC user extension, it provides a simple, consistent interface to any list of 1 or more HTML form elements.
Basically, you set one or many values, click one or many controls, or read the whole form with a single call, always using the same sweet syntax.
When would I use this?
When you have lots of testing to do on:
-
many forms
-
large forms
-
complex forms
-
dynamic forms
HoneyDo was born of the need to test a large, input-heavy app with over 350 Oracle tables at the backend and over 100 input screens in the front. The business domain and UI workflows are complex, so the tools for making the functional tests needed to be as lightweight and flexible as possible. Things like GUI maps and screen driver objects are heavyweight, rigid, and brittle: the amount of maintenance they require keeps you from getting your tests right.
When data matters, but widgets don’t
In testing app functionality through the UI, it often doesn’t make a difference if you type, select, check, or click: you just need to get values to the server.
Examples
With a SeleniumDriver . . .
browser = Selenium::SeleniumDriver.new('localhost', 44...
Click things
# submit your form
browser.set_form_values(:saveButton)
# toggle a bunch of checkboxes and submit
browser.set_form_values( [:include_documents, :calculate_fee, :require_guarantor_address, :fasb_13_ok, :120_day_rule, :UPDATE] )
Set things
browser.set_form_values(:user_name => "pprubens", :password => "hy!Bosch53xy")
browser.set_form_values(:state => "TN", :street_1 => "9113 Foo St.", :street_2 => "4D", :city => "Erlewine", :postal => 99999, :same_as_shipping => true)
Read things
default_values = browser.get_form_values("newProprietorshipForm")
assert_equal({:state => "AL",
:typeOfBusiness => "Please Select",
:taxjurisdiction => "Federal",
:corporation => "on",
:submitButton => "Save",
:cancel => "Cancel"}, default_values, "only default fields populated?")
visible_fields = browser.get_form_elements("newProprietorshipForm").split(',')
assert_equal(visible_fields.size - default_values.size, 17, "count of unset fields on initial page load")
How does it work?
The key to HoneyDo#set_form_values is that each HTML form
element knows its own type. If we identify the element, the code knows what to do.
For each element in our input list:
1 - find the element
The form.elements
collection is a JavaScript associative array, so we can use either its array index or its name
attribute.
For a generic js example, these will all have the same effect if selectDealer
is the 3rd element:
element = myForm.elements['selectDealer'];
element = myForm.elements[2];
element = myForm.elements['2'];
2 - get its type
element.type
3 - set the value (or call click()
)
The type
could be:
- settable:
(text, textarea, password)
-
The
value
attribute is set - selectable:
(select-one, select-mulitple)
-
The
option
index is found and theselected
attribute is set - clickable:
(button, submit, reset, radio)
-
click()
is called. - checkbox:
(checkbox)
-
the
checked
attribute is set orclick()
is called
When element.type
has no value, then element
is itself a collection of other elements, i.e., a group of checkboxes or radio buttons with the same name
. In this case click()
is called on the correct item in the goup.
4 - fire event handlers
Any or all of these are fired, if present:
onclick()
onchange()
onblur()
Ajax
Nothing explicit is done to wait for Ajax calls. On HoneyDo’s home app, we find that in IE the built-in retry
is enough to handle the occasional glitch. In Firefox, multiple call mode using the :one_field_at_a_time
option is required, but does in fact work.
We’ve tried the prototype.js Ajax.activeRequestCount
trick, but couldn’t figure out how get a reference to the right Ajax
instance.
I make no guarantees about how this will work with your Ajax calls, but I’d suggest trying :one_field_at_a_time => true
for your Ajax pages and just leaving it alone otherwise. If you need an explicit wait and have the solution, JavaScript Form_.prototype.set_value()
would be the place to call it from.
Installation
There’s a manual step.
The Implementation
Selenium user extensions that aren’t implemented as methods on the Selenium
JavaScript prototype object might be odd.
However, some of these methods require pre- or post-processing in Ruby, so I couldn’t think of any other way to do it.
I’m always open to better solutions. I’m more interested in propagating the list-driven technique wherever it’s applicable than in preserving any particular encoding of it.
Blessings upon whoever came up with SeleniumDriver.get_eval()
!
The Tests …
… are the documentation. They detail the features and limitations. Run them and read them.
See running_the_tests
Why be collection-driven?
More testing, less code
As a functional test automator and automation lead, I want my scripts, scripters, and self to do more of the testing work that matters, in less time, with less code to distract from finding bugs.
At run time, scripts go faster when all of the fields are set or read in one go, rather than with an individual Selenium call for each field. At development and maintenance time, the code is streamlined considerably by removing the usually unnecessary layer of “action” commands. And the scripters’ work goes faster when there is only one API to learn, as opposed to many different ones for all of the different widget types.
User emulation is waste
Named actions (click, select, toggle, etc) that require us to identify the type of control being called and to specify the arguments to each command differently derive from the assumption that functional test automation implies or requires end-user emulation. Emulating human users and testing app functionality are two different programming problems, and while user emulation is certainly possible it rarely adds anything but clutter, confusion, and complexity to the functional testing effort. Unless there is an explicit testing need to make assertions about the mechanical details of interacting with widgets in type-specific ways, then we are better off without named actions.