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.

    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?)

    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

    namespace :test do
      Rake::TestTask.new(:selenium) do |t|
        t.libs << "test"
        t.pattern = 'test/selenium/**/*_test.rb'
        t.verbose = true
      end
      Rake::Task['test:selenium'].comment = "Run the selenium tests in test/selenium"
    end

    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.

  • 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.

    # 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 < Test::Unit::TestCase
      def setup
        @verification_errors = []
        @browser = Selenium::Client::Driver.new "localhost", 4444, "*firefox", "http://localhost:3000", 10000
        @browser.start_new_browser_session
        
        @authentication_service ||= OurCustomAuthServerClientConnectionStuff()
      end
      
      def teardown
        @browser.close_current_browser_session
        assert_equal [], @verification_errors
      end
      
      def test_login_success
        @user, password = create_active_basic_sop_user
    
        @browser.open "/"
        @browser.type "email", @user.email
        @browser.type "password", password
        @browser.click "//input[@name='commit' and @value='Login']", :wait_for => :page
        begin
          assert @browser.is_element_present("logout")
        rescue Test::Unit::AssertionFailedError
          @verification_errors << $!
        end
      end
    end

    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.

    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

    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.

    Another thing that is important here is the two generate functions. These come from the random_data 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.

Post a Comment

Your email is never published nor shared. Required fields are marked *