Rails and Selenium
I’ve mentioned our crazy SOA architecture here a couple times in the past, but essentially, the platform we are moving towards is a whole bunch of shared services which drive a diverse set of front-ends. In any setup like this, the big risk is a change to an underlying service which manifests itself in one or many of the front-ends. To try and mitigate the risk, and testing effort involved I am creating Selenium scripts around the bits that are service related. This post is a how-to on using Selenium with Rails in a sane, extensible manner. Well, as sane as anything that is in Rails-land at any rate.
- Get Selenium Running – Since this is Rails we’re talking about, we’re going to be scripting in Ruby, so you need to install the Ruby client.
<pre lang="bash">sudo gem install selenium-client
You will also need to get the Selenium Server; of which release 1.0 was finally finished after 5 years. Start the server.
- Start your application – This might seem to an obvious step, but there is a trick that makes it worth mentioning and that is you need to start it in ‘test’ mode. If you do not, then your manipulations in the Selenium script will go against one database and the browser will go to another. Thats a bit of brain pain you don’t want. Trust me. (Care to guess how I spent my day?)
<pre lang="bash">script/server -etest
- Get your Rake task ready – To be fully Rails-ish, we will want to run our scripts using Rake. So create in lib/tasks a file called selenium.rake with the following task
<pre lang="ruby">namespace :test do Rake::TestTask.new(:selenium) do |t| t.libs <p> There are variations of this out there that include a pre-requisite task of db:test:prepare, but we don't want that as our server is already running in test mode and wiping out tables from underneath it is not a good idea.</p>
- Give yourself a helper - Rails comes with a file called tests/test_helper.rb; create a copy of it and call it selenium_helper.rb. And while you are at it, set self.use_transactional_fixtures to false. This results in us leaving stuff in the database after a run, but we are smart enough to clean up after ourselves when necessary and to use unique values whenever possible.
- Record and start hacking - I detailed the steps an automated script should go through in this post, but I’m jumping straight to the end of having your scripts be intelligent. Here is the script, which needs to be named something_test.rb for the Rake task to work.
<pre lang="ruby"># include our helper require File.expand_path(File.dirname(__FILE__) + "/../selenium_helper") # selenium require 'rubygems' gem "selenium-client", ">=1.2.15" require "selenium/client" class LoginTest :page begin assert @browser.is_element_present("logout") rescue Test::Unit::AssertionFailedError @verification_errors <p> Here is the helper, minus all the stuff that come with it originally since just did a copy/paste originally. That stuff is still in the file, just not here.<br></br> </p> <pre lang="ruby">require 'random_data' def create_active_basic_sop_user z_user, password = create_basic_sop_user z_user = activate_user(z_user) return z_user, password end def create_basic_sop_user # create local user my_password = generate_password my_attrs = {"email" => generate_email, "password" => my_password, "password_confirmation" => my_password} @user = User.new(my_attrs) if not @user.save assert_false end # give it some user_types user_type = UserType.new user_type.user = @user user_type.role = Role.find_by_rolename('user') # the role might not actually exist so create it if not if not user_type.role u = Role.new u.rolename = 'user' u.save user_type.role = Role.find_by_rolename('user') end user_type.save return @user, my_attrs["password"] end def activate_user(z_user) z_user = @authentication_service.lookup_user_by_email(FRONTEND, z_user.email) z_user = @authentication_service.activate_user(FRONTEND, z_user) return z_user end def generate_email Random.email end def generate_password Random.alphanumeric end <p> Notice how all the environment logic is in the helper. A common pattern of Selenium success is to wrap it in a DSL. This isn't really at this point, but you get all the benefits of code reuse, etc.. Note as well how small the functions are so you can build even more useful abstractions of as the effort continues.<br></br> <br></br> Another thing that is important here is the two generate functions. These come from the <a href="http://rubyforge.org/projects/random-data/">random_data</a> gem and allow for unique test data every time through. (Or at least gives you a much greater chance at uniqueness.) These help us negate the Mine Field Problem and Pesticide Paradox and allows us to be slightly sloppy about cleaning up after ourselves. This slop is a design choice which will mean more database (detailed) interaction down the road when it comes to validation, but that is likely a good thing in and of itself.</p>