December 5, 2011

Testing old web pages with new tools


Recently I discovered a great tool for doing acceptance and unit tests of the web interfaces named Capybara. It works nicely as a back-end to Cucumber / Gherkin, but it is possible, if need be, to be used stand-alone. My goal was to validate an old page we'd created using ASPX.Net  One thing, though, some folks might see as a draw-back: it’s a Ruby tool.

Capybara

Capybara

Installing the tools gave me a surprising amount of difficulties on my Ubuntu 11.10 box. I’d expected running gem install would do the job, but there are other pre-requisits, too (as described here). First I did something like

$ sudo apt-get install ruby ruby-dev ruby1.8 ruby1.8-dev ruby1.9.1 \
    ruby1.9.1-dev rubygems rubygems1.8

I am aware that some of these are redundant, but I also want to also have a 1.9 Ruby. Here are some more dependencies that I needed to throw at my box:

$ sudo apt-get libxml-parser-ruby libxml-parser-ruby1.8 \
    libxml-parser-ruby1.9.1 ruby-xmlparser libxml2-dev \
    libxml-ruby1.8 libxslt-ruby libxslt-ruby1.8 libxslt1-dev

Now I could have a go with the actual library installation:

$ sudo gem install gherkin cucumber capybara

and, for a good measure, also

$ sudo gem1.9.1 install gherkin cucumber capybara

Along the way, in each cases, I received errors such as:

Invalid gemspec in [/var/lib/gems/1.8/specifications/cucumber-1.1.1.gemspec]:
  invalid date format in specification: "2011-10-30 00:00:00.000000000Z"

so I had to open each of the problematic gemspec and modify it as suggested here by replacing the string such as %q{2011-10-30 00:00:00.000000000Z} to a shorter version %q{2011-10-30}.

Finally ready for action, I could now have my Ruby script visit a page at some URL, enter a user name and password, and click a button for me.

# encoding: UTF-8

require "rubygems"
require "capybara"

base_url = "https://localhost:4430/myWebApp/"
session = Capybara::Session.new(:selenium)
session.visit(base_url + "Login.aspx")
session.fill_in("LogInTextBox", { :with => "matej" })
session.fill_in("PasswordTextBox", { :with => "mysecretpassword" })
session.click_on("LoginButton")

Having used the Selenium driver, the script, when run, starts its own instance of FireFox and browses the web application as instructed.

However, my problem was that, as one can tell from the snippet, I needed to run tests on an old ASPX.Net web application. There, not all the controls have nice descriptive names or IDs to be uniquely used. Instead, we have tables of labels and input components to be filled in selectively. There, the ASPX’s engine would render the input fields using a series of unique IDs such as ctl00$TextBox_RowQuantity_25_3. I guess the appended numbers mean the row and the column index of the table cell where the control is, but I would rather not go and guess the actual rule of composing this ID. The bottom line is, the ID was never meant to be used by any other machine logic other than the one that produced it, and that one, of course, knows how to sort them out.

To test such a page, the script must use the clues visible to the user. This means going by the content of the labels in the table to figure out the specific row where the user would want to input some quantity or manipulate a control. For example, we may have a column with a person’s name, another one with their address, and a column with an input field for, say, a number of cats owned.  It is also valid to know the column number of the target fields, i.e., the field is in the column 4.

By involving the XPath magic, we can have Capybara locate the column with the target field, extract the ID or the name of the field, and then fill the data in as usual.

clientName = "Joe Doe"
rowEls = session.all(:xpath, "//tr[td=\\"#{clientName}\\"]")

raise "No record match for #{clientName}" if rowEls.size <= 0

rowEls.each do |el|
    inputName = el.find(:xpath, 'td/input[@type="text"]')["name"]
    el.fill_in(inputName, { :with => "3" })
end

This works if the field we need is the first (or the only) text input field. If that is not the case, specifying the column number with the field is also possible using the XPath:

inputName = el.find(:xpath, 'td[3]/input[@type="text"]')["name"]