Refactoring tests using Test-Unit and DRY

Introduction

This is a very basic introduction to the design of a Watir test suite. Thanks to FinnTechAS for contributing this example.

Driving IE with Watir

The Watir library (a Ruby gem) lets you control Internet Explorer.

require "rubygems"
require "watir"

ie = Watir::IE.new
ie.goto("http://google.com")
ie.text_field(:name, 'q').value = "Watir"
ie.button(:name, 'btnG').click

This script will go to Google, type "Watir" in the search box and click the Search button.
As you see from the example above, Watir lets you identify objects in the IE DOM using the syntax .element_type(how, what).
In this example we tell it to look for a text field where the name attribute is set to q. When we call the Watir::IE#text_field method, it will return an instance of Watir::TextField. Let's add a couple of lines to our script:

if ie.contains_text("Watir is a simple open-source library for automating web browsers")
   puts "yes, found the text"
else
   puts "no, couldn't find the text"
end

ie.close

This version will check if the result list contains the text we expect, print out a relevant message depending on the result (puts means 'put string'), then close the IE window. The complete script now looks like this:

require "rubygems"
require "watir"

ie = Watir::IE.new
ie.goto("http://google.com")
ie.text_field(:name, 'q').value = "Watir"
ie.button(:name, 'btnG').click

if ie.contains_text("Watir is a simple open-source library for automating web browsers")
   puts "yes, found the text"
else
   puts "no, couldn't find the text"
end

ie.close

Dowload this example: google-extended1.rb

Let's save this to a file called watir_tc_google.rb and run it from the command line:

$ ruby watir_tc_google.rb
yes, found the text
$

Test::Unit

This is (almost) the same test using Ruby's built-in Test::Unit framework:

require "rubygems"
require "watir"
require "test/unit"

class TestGoogle < Test::Unit::TestCase

   def setup
      @ie = Watir::IE.new
   end

   def test_watir_search
      @ie.goto("http://google.com")
      @ie.text_field(:name, 'q').value = "Watir"
      @ie.button(:name, 'btnG').click
      assert(@ie.contains_text("Watir is a simple open-source library for automating web browsers"), "Couldn't find text: Watir is a simple...")
      assert(@ie.contains_text("Ruby rocks!"), "Couldn't find text: 'Ruby rocks!'")
   end

   def teardown
      @ie.close
   end

end

Dowload this example: google-extended2.rb

We're creating a test case called TestGoogle, which is a subclass of Test::Unit::TestCase. The Test::Unit framework will look for and run all the methods that start with test (in this case, it's only one; test_watir_search). The setup and teardown methods are automatically called by the framework before and after every test method. It also provides some useful methods like assert, assert_equal, among others, which can be given a failure message argument that will be added to the test output if the assertion fails. Notice that the ie variable now is called @ie. The @ sign makes it an instance variable - the scope is thus not local to the setup method, but lets our test_watir_search method access the variable as well.

The framework will also automatically run the test case if we execute the file. Here's the output when we run it from the command line:

$ ruby watir_tc_google.rb
Loaded suite watir_tc_google
Started
F
Finished in 3.487 seconds.

  1) Failure:
test_Watir_search(TestGoogle) [watir_tc_google.rb:16]:
Couldn't find text: 'Ruby rocks!'.
<nil> is not true.

1 tests, 2 assertions, 1 failures, 0 errors
$

Oops, our second assertion failed. Perhaps someone should fix that. For now, we'll just make our test pass by replacing assert with assert_nil, and change the failure message accordingly:

def test_watir_search
  @ie.goto("http://google.com")
  @ie.text_field(:name, 'q').value = "Watir"
  @ie.button(:name, 'btnG').click
  assert(@ie.contains_text("Watir is a simple open-source library for automating web browsers"), "Couldn't find text: Watir is a simple...")
  assert_nil(@ie.contains_text("Ruby rocks!"), "Couldn't find text: 'Ruby rocks!'")
end

Dowload this example: google-extended2-fixed.rb

nil is Ruby's idea of 'no value', and incidentally it's exactly what the contains_text method will return if it doesn't find the text we give it. Now let's see:

$ ruby watir_tc_google.rb
Loaded suite watir_tc_google
Started
.
Finished in 3.975 seconds.

1 tests, 2 assertions, 0 failures, 0 errors
$

That's better.

Avoiding duplication (DRY)

Reference: See Wikipedia's entry for Don't Repeat Yourself (DRY)

We now have a test that searches Google and looks for a specific text in the result list. But what if we want to search for another text? Perhaps we want to learn about some popular programming paradigms. We could do it like this:

require "rubygems"
require "watir"
require "test/unit"

class TestGoogle < Test::Unit::TestCase

  def setup
    @ie = Watir::IE.new
  end

  def test_watir_search
    @ie.goto("http://google.com")
    @ie.text_field(:name, 'q').value = "Watir"
    @ie.button(:name, 'btnG').click
    assert(@ie.contains_text("Watir is a simple open-source library for automating web browsers"), "Couldn't find text: Watir is a simple...")
    assert_nil(@ie.contains_text("Ruby rocks!"), "Found unexpected text: 'Ruby rocks!'")
  end

  def test_dry_search
    @ie.goto("http://google.com")
    @ie.text_field(:name, 'q').value = "don't repeat yourself"
    @ie.button(:name, 'btnG').click
    assert(@ie.contains_text("(DRY) is a statement exhorting developers to avoid"))
  end

  def teardown
    @ie.close
  end

end

Dowload this example: google-extended3-new-test.rb

This works fine - but if for some reason the name attribute of Google's search button (or anything else on the page) changes we'll have to edit the script twice, or maybe more if we create more tests. Let's extract the relevant code to a method, that takes the search query as an argument:

def simple_search(query)
   @ie.goto("http://google.com")
   @ie.text_field(:name, 'q').value = query
   @ie.button(:name, 'btnG').click
end

Now our script looks like this:

require "rubygems"
require "watir"
require "test/unit"

class TestGoogle < Test::Unit::TestCase

  def setup
    @ie = Watir::IE.new
  end

  def test_watir_search
    simple_search("Watir")
    assert(@ie.contains_text("Watir is a simple open-source library for automating web browsers"), "Couldn't find text: Watir is a simple...")
    assert_nil(@ie.contains_text("Ruby rocks!"), "Found unexpected text: 'Ruby rocks!'")
  end

  def test_dry_search
    simple_search("don't repeat yourself")
    assert(@ie.contains_text("(DRY) is a statement exhorting developers to avoid"))
  end

  def teardown
    @ie.close
  end

   #========= HELPER METHODS =========
   private

  def simple_search(query)
    @ie.goto("http://google.com")
    @ie.text_field(:name, 'q').value = query
    @ie.button(:name, 'btnG').click
  end
end

Dowload this example: google-extended4.rb

Creating a class

Adding helper methods to the test case is a step forward, but we'll run into problems if we want to use our simple_search method in another file (that is, another test case). Eventually we'll also want to store and reuse more information (like error messages, database connections), specific to some domain (like the app under test). The solution is to create a class. Here's what our Google test looks like, refactored:

require "rubygems"
require "watir"
require "test/unit"

class Google
   attr_accessor :ie

   def initialize
      @ie = Watir::IE.new
   end

   def simple_search(query)
      @ie.goto("http://google.com")
      @ie.text_field(:name, 'q').value = query
      @ie.button(:name, 'btnG').click
   end

   def login_gmail
      # code
   end

   def check_calendar(date)
      # code
   end

   def close
      @ie.close
   end

end


class TestGoogle < Test::Unit::TestCase
   def setup
      @site = Google.new
   end

   def test_watir_search
      @site.simple_search("Watir")
      assert(@ie.contains_text("Watir is a simple open-source library for automating web browsers",
        "Couldn't find text: Watir is a simple..."))
      assert_nil(@site.ie.contains_text("Ruby rocks!", "Found unexpected text: 'Ruby rocks!'"))
   end


   def test_dry_search
      @site.simple_search("don't repeat yourself")
      assert(@site.ie.contains_text("a statement exhorting developers to avoid duplication in code"))
   end

   def teardown
      @site.close
   end
end

Dowload this example: google-extended5.rb

Our class is called Google, and the idea is to encapsulate everything we might want to do on that site - like log in to Gmail or check our appointments for a specific date. The initialize method is what gets called when we later instantiate the class (Google.new in the test case). We are wrapping the Watir::IE object inside our Google class, so when we want to call it from our test scripts, we need to use @site.ie (assuming the variable holding our Google instance is called @site). By typing "attr_accessor :ie" at the top of our class, Ruby will automatically create methods for accessing or changing this instance variable ("getter" and "setter" methods).

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Jun 13, 2008

    Alan Baird says:

    "require 'rubygems'" is not needed if you are using the One Click Installer sinc...

    "require 'rubygems'" is not needed if you are using the One Click Installer since it is automatically required.

  2. May 10, 2010

    Mark Hampton says:

    Hi, I wanted to have an example running on Firefox. The new Browser interface ma...

    Hi, I wanted to have an example running on Firefox. The new Browser interface makes for an easy way to support both IE and Firefox. Also Google seems to have changed and we need to wait for the search results to be displayed (it seems). Below is the example with those changes ans the tests passing.

    require 'rubygems' require 'rubygems'
    require 'firewatir'
    require 'test/unit'
    
    Watir::Browser.default = "firefox"
    
    class Google
      attr_accessor :b
    
      def initialize
        @b = Watir::Browser.new
      end
    
      def simple_search(query)
        @b.goto("http://www.google.com/en")
        @b.text_field(:name, 'q').value = query    @b.button(:name, 'btnG').click
        Watir::Waiter::wait_until { @b.text.include? 'Search Results' }
      end
    
      def login_gmail
        # code
      end
    
      def check_calendar(date)
        # code
      end
    
      def close
        @b.close
      end
    
    end
    
    
    class TestGoogle < Test::Unit::TestCase
      def setup
        @site = Google.new
      end
    
      def test_watir_search
        @site.simple_search("Watir")
        assert(@site.b.text.include?("Watir is a family of drivers"), "Couldn't find text: Watir is a family...")
        assert_equal(false, @site.b.text.include?("Ruby rocks!"), "Found unexpected text: 'Ruby rocks!'")
      end
    
    
      def test_dry_search
        @site.simple_search("don't repeat yourself")
        assert(@site.b.text.include?("Don't Repeat Yourself (DRY)"))
      end
    
      def teardown
        @site.close
      end
    end