Archive for July, 2008

How to write a ruby function that can accept either a string or a regular expression

July 30, 2008

While working on our unit test framework, I wanted to write a function that checks if a given Ext JS windoid is opened. The function should accept a title argument. This was easily implemented as:

	# title is a string with the exact windoid title
	def windoidDisplayed?(title)
		spans = $browser.spans
		t = $browser.length && $browser.detect {|i| \
			i.class_name == 'x-window-header-text' \
			&& title == i.text \
		}
		return t && t.visible?
	end

I then realized that users might not have the full title of the window, and might only want to check for a windoid with a title that matches some regular expression. A simple and naive way to do it is

	# title can be a string or a regular expression
	def windoidDisplayed?(title)
		spans = $browser.spans
		t = $browser.length && $browser.detect {|i| \
			i.class_name == 'x-window-header-text' \
				&& (title.kind_of?(String) \
					? (title == i.text) \
					: (title =~ i.text)) \
		}
		return t && t.visible?
	end

However, I didn’t want to check in every loop iteration if the passed title argument is regular expression. Although performance is really a non-issue in our unit tests, it just seemed to be a cumbersome and ugly code. I thought of using the dynamic nature of ruby to add a method (e.g. “match?”) to both string and regular expression classes, so I can call it in a polymorphic way without having to detect the type of title:

	class String
		def match?(x)
			return self == x
		end
	end
	
	class Regexp
		def match?(x)
			return self.match(x)
		end
	end
	
	# title can be a string or a regular expression
	def windoidDisplayed?(title)
		spans = $browser.spans
		t = $browser.length && $browser.detect {|i| \
				i.class_name == 'x-window-header-text' \
				&& title.match?(i.text) \
		}
	return t && t.visible?

Before implementing it, I thought I’d check if a similar function already exists in Ruby’s Standard Library, and indeed, when I looked up the Ruby book for functions that are in both String and Regexp classes, I run into “===” (the Case Equality operator), which is meant exactly for this kind of things. Unlike JavaScript, where the === operator is meant for identity, in Ruby, the === method is usually called in case expressions to match the case target for each when clause. As said in the documentation, for the String class, === method tests for equality and is the same as ==, but for Regexp class, === method tests for matches, and is the same as =~. Exactly what I was looking for. So my little function can simply be implemented like:

	# title can be a string or a regular expression
	def windoidDisplayed?(title)
		spans = $browser.spans
		t = $browser.length && $browser.detect {|i| \
				i.class_name == 'x-window-header-text' \
				&& title === i.text \
		}
		return t && t.visible?
	end

BTW, while searching for that, I took a look at Watir‘s sources, hoping to learn from the experts as I’m new to Ruby.
I found that Watir source (watir.rb, lines 50-70) does a similar trick to what I initially thought to do – define a new “matches” method (and not “match?” like the ruby convention that is also used in Watir in methods like “exists?” and “visible?“), and don’t use the === method.
In addition, in several places in the code there are still explicit tests for String class, such as the verify_contains method in input_elements.rb and navigate_to_link_with_id and navigate_to_link_with_url methods in watir_simple.rb.

Handling browser on test start and end

July 23, 2008

When I started building a unit tests framework for our product, I decided that I want the browser to remain open through all tests, and only be closed at the end. This is because I don’t want a developer running these tests on his machine to be disturbed with many browser windows opened and closed.

However, as I quickly found out, there is no easy way to run code at the beginning and end of a test suite. This seems to be intentional, as each test case should be separate and independent of other tests in the suite. The setup and teardown methods are run for each test case.

One simple solution was to drop it and simply start the browser on every test case and close it on end, but when I played with it a little more, I reached an even better solution. I’m starting the browser at the beginning of the test and assign it to a global $browser. Then, if the test fail (which can be detected by @test_passed), we set $browser to nil, so the browser window remains, and subsequent tests create and use a new browser window. If the test succeed, the browser window is reused through $browser. At the end of the test, we close the browser if $browser is not nil. With this constellation, after the test suite finishes running, we have a browser open for each failed test, so we can inspect what went wrong and how the application looks at the time of the fail.

Here is the code I used to implement it:

# cleanup on close
END { # close browser at completion of the tests
$browser.close if $browser && $browser.exists? && !ENV[‘DONT_CLOSE_BROWSER_AT_END’]
Watir::IE.quit
}

module MyTestSuite < Watir::TestCase ############################################################# # setup method: # This setup function is called by the UnitTest framework before any test is run. # The function initialize the global $browser object if needed ############################################################# def setup return if $browser $browser = Watir::IE.new $browser.speed = :fast $browser.add_checker(PageCheckers::NAVIGATION_CHECKER) $browser.maximize unless ENV['DO_NOT_MAXIMIZE_BROWSER'] end ############################################################# # teardown method: # This teardown function is called by the UnitTest framework after any test is run. # The function reset the global $browser object in case an error # occurred (unless NO_NEW_BROWSER_ON_ERROR), so # a new browser window will be created in case an error occurred. ############################################################# def teardown $browser = nil unless @test_passed || ENV['NO_NEW_BROWSER_ON_ERROR'] end # ... test cases methods goes here end [/sourcecode]