不过既然用了Ruby,总要用一些ruby sugar吧,我们定义一个on方法来表达页面操作的环境:
def on page_type, &block
page = page_type.new $selenium
page.instance_eval &block if block_given?
end
之后我们就可以使用page object的类名常量和block描述在某个特定页面上操作了:
on LoginPage do
login_as :name =>
'xxx', :password =>
'xxx'
end
除了行为方法之外,我们还需要在page object上定义一些获取页面信息的方法,比如获取登录页面的欢迎词的方法:
def welcome_message
@driver.get_text 'xpath='
end
这样测试也可表达得更生动一些:
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name =>
'xxx', :password =>
'xxx'
end
当你把所有的页面都用Page Object封装了之后,就有效地分离了测试和页面结构的耦合。在测试中,只需使用诸如login_as和add_product_to_cart这样的业务行为,而不必依靠像id、name等这些具体且易变的页面元素了。当这些页面元素发生变化时,只需修改相应的page object就可以了,而原有测试基本不需要太大或太多的改动。
2. Assertation只有行为还构不成测试,我们还要判断行为结果,并进行一些断言。简单回顾一下上面的例子,会发现还有一些很重要的问题没有解决:我怎么判断登录成功了呢?我如何才能知道真的是处在登录页面了呢?如果我调用下面的代码会怎样呢?
$selenium.open url_of_any_page_but_not_login
on LoginPage {}
因此我们还需要向page object增加一些断言性方法。至少,每个页面都应该有一个方法用于判断是否真正地达到了这个页面,如果不处在这个页面中的话,就不能进行任何的业务行为。下面修改LoginPage使之包含这样一个方法:
LoginPage.class_eval do
include Test::Unit::Asseration
def visible?
@driver.is_text_present() && @driver.get_location ==
end
end
在visible?方法中,我们通过对一些特定的页面元素(比如URL地址,特定的UI结构或元素)进行判断,从而可以得之是否真正地处在某个页面上。而我们目前表达测试的基本结构是由on方法来完成,我们也就顺理成章地在on方法中增加一个断言,来判断是否真的处在某个页面上,如果不处在这个页面则不进行任何的业务操作:
def on page_type, &block
page = page_type.new $selenium
assert page.visible?, "not on #{page_type}"
page.instance_eval &block if block_given?
page
end
这个方法神秘地返回了page对象,这里是一个比较取巧的技巧。实际上,我们只想利用page != nil这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name =>
'xxx', :password =>
'xxx'
end
assert on WelcomeRegisteredUserPage
除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:
def cart_empty?
@driver.get_text('xpath=') ==
'Shopping Cart(0)'
end
需要注意的是,虽然我们在page object里引入了Test::Unit::Asseration模块,但是并没有在断言方法里使用任何assert*方法。这是因为,概念上来讲 page object并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使page object变成针对某些场景的test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为boolean的方法。
3. Test Data测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:
on LoginPage do
login_as :name =>
'userA', :password =>
'password'
end
assert on WelcomeRegisteredUserPage
registered_user = {:name =>
'userA', :password =>
'password'}
on LoginPage do
login_as registered_user
end
assert on WelcomeRegisteredUserPage
def get_user identifier
case identifier
when :registered then return USER_A
when :not_registered then return USER_B
end
end
end
在这里,我们将测试案例和具体数据做了一个对应:userA是注册过的用户,而userB是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改DataFixture模块就可以了,而不必修改相应的测试:
include DataFixtureDat
user = get_user :registered
on LoginPage do
login_as user
end
assert on WelcomeRegisteredUserPage
当然,在更复杂的测试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:
def get_user identifier
case identifier
when :registered then return User.find '.'
end
end
4.Navigator与界面元素类似,URL也是一类易变且难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data相似,这里就不赘述了,下面是一个例子:
navigate_to detail_page_for @product
on ProductDetailPage do
.
end
5. Shortcut前面我们已经有了一个很好的基础,将Selenium测试与各种脆弱且意图不明的元素分离开了,那么最后shortcut不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的helper:
def should_login_successfully user
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as user
end
assert on WelcomeRegisteredUserPage
end
然后是另外一个magic方法:
def given identifer
words = identifier.to_s.split '_'
eval "get_#{words.last} :#{words[0..-2].join '_'}"
end
之前的测试就可以被改写为:
def test_should_xxxx
should_login_successfully given :registered_user
end
这是一种结论性的shortcut描述,我们还可以有更behaviour的写法:
def login_on page_type
on page_type do
assert_equal 'Welcome!', welcome_message
login_as @user
end
end
def login_successfully
on WelcomeRegisteredUserPage
end
def given identifer
words = identifier.to_s.split '_'
eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"
end
最后,测试就会变成类似验收条件的样子:
def test_should_xxx
given :registered_user
login_on LoginPage
assert login_successfully
end