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 the selected attribute is set

clickable:(button, submit, reset, radio)

click() is called.

checkbox: (checkbox)

the checked attribute is set or click() 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.