microphone

UI Testing with Watir and Sinatra

Problem

I’d set out to solve the problem of attaching automation to an already running browser.

The reason was:  a set of UI tests I’d written were self-contained, and had the possibility of getting unwieldy, difficult to understand or debug.

Wasn’t my best work, but we’re trying to put together a proof of concept quickly.

A proposed solution was to take smaller scripts, and attach them to a browser, so that we could see what was going on.

So for example: have a “click” script, an “entertext” script, etc., each responsible for an action, kept small, and by virtue readable since the scripts are named for what they do.

The scripts were meant to attach to a running browser, do their job, and then fall off. Like a tick!

But the whole “attach to a running browser” thing hasn’t really been completely solved. It’s been solved partially with updates to Watir and Selenium, but even so, support is kinda spotty and is reported to work just with IE.

Solution

So instead of attaching to a browser, what if we can wrap a webservice around the browser object, to maintain it for us, and handle incoming HTTP requests as commands.

In effect, we can make a “middle man” that acts on the browser object on our behalf.

Good Side Effects

Some interesting side effects happen when we do this:

  • Allows us to divide tests into smaller pieces so we can see what’s going on,
  • Lets multiple users run tests if desired,
  • Gives the ability to run the same test against multiple browsers,
  • Cut down on the amount of code required to write a test, and
  • Decrease the amount of software dependency for test automation.

How it Works

A webservice is a chunk of code that runs on a machine, and does work based on incoming HTTP requests.

Sinatra is a lightweight Ruby library that allows for rapid construction of webservices. And Watir is a library for browser-based UI testing in Ruby.

We’re gonna take these two technologies, and smash ’em together and see what happens.

Writing our First Webservice

First, if you haven’t already, install the Sinatra and Watir gems, like this:

gem install sinatra
gem install watir

To write a webservice, we need to specify our routes.

A route is like a command—when the webservice receives an HTTP request, it decides what to do, based on what kind of request it received.

For this demo we’ll write 4 routes—one to open the browser, one to go to a URL, one to enter text into a certain text field and then one to close the browser.

Here’s how it looks:

require 'rubygems'
require 'sinatra'
require 'watir-webdriver'

set :port, 9000

post '/openbrowser' do
 $browser = Watir::Browser.new :chrome
 $browser.driver.manage.window.maximize
end

post '/goto' do 
 $browser.goto(params[:url])
end

post '/closebrowser' do 
 $browser.close
end

post '/entertext' do
 $browser.text_field(:xpath => "//input[@id='lst-ib']").when_present.set(params[:text])
end

Most of this should look straightforward. The three things I want to point out are:

  • There’s a global variable called $browser that will hold our browser instance for us,
  • The service will run on port 9000, and
  • For this part of the demo, the text_field we write to is hardcoded to the one at google.com.

Save this as a script called sinatra.rb.

Next we write the script that will actually send the requests to this webservice, along with some short pauses so we can see what’s going on:

require 'net/http'
require 'uri'

# open the browser
url = 'http://localhost:9000/openbrowser'
uri = URI.parse(url)
params = {}
Net::HTTP.post_form(uri, params)

# goto a site
url = 'http://localhost:9000/goto'
uri = URI.parse(url)
params = {url: "www.google.com"}
Net::HTTP.post_form(uri, params)

sleep(5)

# enter some text
url = 'http://localhost:9000/entertext'
uri = URI.parse(url)
params = {text: "ruby sinatra"}
Net::HTTP.post_form(uri, params)

sleep(5)

# close the browser
url = 'http://localhost:9000/closebrowser'
uri = URI.parse(url)
params = {}
Net::HTTP.post_form(uri, params)

For each of these 4 commands, we’re sending the HTTP request to localhost, since we’ll be running the webservice on a local machine.

Each request has a route (openbrowser, goto, entertext or closebrowser), and parameters of some kind. Sometimes the parameters are empty, such as for opening or closing the browser, but others send extra data to the webservice.

Save this file, calling it net_http.rb.

Running the Things

To get started, open up a command prompt and navigate to where you saved sinatra.rb, then run “ruby sinatra.rb” to start up the webservice. You’ll see some information about the webservice:

C:\Users\Fritzius\Desktop\Projects\Sinatra Watir Framework>ruby sinatra.rb
[2016-09-26 20:13:06] INFO WEBrick 1.3.1
[2016-09-26 20:13:06] INFO ruby 2.0.0 (2015-08-18) [i386-mingw32]
== Sinatra (v1.4.7) has taken the stage on 9000 for development with backup from WEBrick
[2016-09-26 20:13:06] INFO WEBrick::HTTPServer#start: pid=6268 port=9000

Leave this terminal window open, and open another one. Navigate to where you saved net_http.rb and run that script by typing ruby net_http.rb.

You’ll see the following things happen:

  • A browser will come up,
  • It’ll go to Google,
  • The phrase “ruby sinatra” will be entered into the text field,
  • The browser will close.

As these requests are sent to the webservice, you’ll see more data about what the webservice is doing with them:

[26/Sep/2016:20:21:38 -0500] "POST /openbrowser HTTP/1.1" 200 - 4.3342
[26/Sep/2016:20:21:33 Central Daylight Time] "POST /openbrowser HTTP/1.1" 200 0
- -> /openbrowser
[26/Sep/2016:20:21:44 -0500] "POST /goto HTTP/1.1" 200 21 6.0883
[26/Sep/2016:20:21:38 Central Daylight Time] "POST /goto HTTP/1.1" 200 21
- -> /goto
[26/Sep/2016:20:21:50 -0500] "POST /entertext HTTP/1.1" 200 - 0.5945
[26/Sep/2016:20:21:49 Central Daylight Time] "POST /entertext HTTP/1.1" 200 0
- -> /entertext
[26/Sep/2016:20:21:56 -0500] "POST /closebrowser HTTP/1.1" 200 - 0.8015
[26/Sep/2016:20:21:55 Central Daylight Time] "POST /closebrowser HTTP/1.1" 200 0
- -> /closebrowser

This output shows that all 4 commands came back successful, indicated by the HTTP status of 200 near the end of each line. You can also see the time taken for each command—ranging from 6.0883s to as low as 0.5945s.

Extras

This can be extended further. We could:

Run the Service on a Different Machine

There’s nothing stopping us from hosting the service on some other machine. It actually helps expose the test framework to people other than ourselves. A person who wants to write a test would just write their requests to point to a new url in your network, instead of localhost:9000.

Host Multiple Instances of the Service

If you’re concerned about one service doing a ton of work, you could spin up another service on a different port. Aim your requests at the new port, such as port 9001, instead of 9000, or whatever you’ve set it to. This will keep one service from hogging up all the resources.

Set Up Pass/Fail Status and Bodies

A test isn’t a test unless it can pass or fail. Fortunately for us, HTTP status codes and response bodies can convey a LOT of information to us. If things are working ok, a simple 200 status meaning “OK” lets us know. But anything else, and you can yell as hard as you want, as descriptively as you want, by stocking the HTTP response body with any text and debug info you want. Google for more information about how to do this.

Break Actions Into Their Own Scripts

Earlier I mentioned having scripts responsible for their own actions to help with readability. It becomes easy to see what a test is doing when you’re calling scripts like click.rb, entertext.rb, select.rb, with readable arguments for each.

Generate Scriptlets at Runtime

The best code to write is no code! How cool would it be to have sinatra.rb parse itself at runtime, and figure out how to write the small scripts for us? Pretty cool, I think. Then we’d only have to focus on the service itself, and let it do the heavy lifting for us.

Cross Browser Testing

Instead of operating against one browser object, we can set up an array of them, and perform the same actions against all of them: Operating against each is a matter of looping through each browser stored in the array:

$browsers = []

post '/openbrowser' do
 b = Watir::Browser.new :chrome
 b.driver.manage.window.maximize
 $browsers.push(b)
end
(...clip...)
post '/goto' do 
 $browsers.each do |b|
  b.goto(params[:url])
 end
end

Use PageObjects

There are many ways to specify and use PageObjects in testing. However we use them, we can pull them in, and make the framework more generic and extendable. Here’s a simple example:

# low-level example of a page object.
# just a hash with a name-to-xpath mapping
$locators = { 
 "search" => "//input[@id='lst-ib']",
}
(...clip...)
post '/entertext' do
   $browser.text_field(:xpath => $locators[params[:element]]).when_present.set(params[:text])
end

Conclusion

I’m pleasantly surprised with this combination of technology. Seems like there are some neat applications for it. Glad to have it in my arsenal.

How can it help you?


It takes fresh ideas and disruptive innovation to compete. 

Picking up tech and slamming them together is sometimes what it takes to make a breakthrough. I want to enable you to try new things. With my consultancy, Arch DevOps LLC, I help level up your team and your company.

Would you like to find out how?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s