应用Seleniumq行W(xu)eb试往往?x)存在几个bad smellQ?br>1.大量使用name, id, xpath{页面元素。无论是功能修改、UI重构q是交互性改q都?x)?jing)响到q些元素Q这使得Selenium试变得非常脆弱?br>2.q于l节的页面操作不Ҏ(gu)体现?gu)为的意图Q一D|间之后就很难真正把握试原有的目的了(jin)Q这使得Selenium试变得难于l护?br>3.对具体数据取值的存在依赖Q当个别数据不再合法的时候,试׃(x)p|Q但q样的失败ƈ不能标识功能的缺失,q得Selenium试变得脆弱且难以维护?br>
而这几点直接衍生的结果就是不断地d新的试Q而极地去重构、利用原有测试。其实这C是正常,单元试试写多?jin),也有会(x)有q样的问题。不q比较要命的是,Selenium的执行速度比较慢(相对单元试Q,随着试逐渐的增多,q行旉?x)逐渐增加C可忍受的E度。一l意图不明难以维护的Selenium试Q可以很L地在每次build的时候杀?0分钟甚至2个小时的旉Q在下就有花2个小时坐在电(sh)脑前面等?50个Selenium试q行通过的?zhn)惨经历。因此合理有效地规划Selenium试显得格外的q切和重要了(jin)。而目前比较行之有效的办法Q往大了(jin)_(d)可以叫domain based web testingQ具体来Ԍ是Page Object Pattern?br>
Page Object Pattern里有四个基本概念QDriver, Page, Navigator和Shortcut。Driver是测试真正的实现机制Q比如SeleniumQ比如WatirQ比如HttpUnit。它们懂得如何去真正执行一个web行ؓ(f)Q通常包含像clickQselectQtypeq样的表C具体行为的Ҏ(gu)QPage是对一个具体页面的装Q它们了(jin)解页面的l构Q知道诸如idQ?nameQ?classQxpathq类实现l节Qƈ描述用户可以在其上进行何U操作;Navigator则代表了(jin)URLQ表CZ些不l页面操作的直接跌{Q最后Shortcut是helperҎ(gu)?jin),需要看具体的需要了(jin)。下面来看一个超U简单的例子——测试登录页面?br>
1. Page Object
假设我们使用一个单独的Login Pageq行dQ那么我们可能会(x)登录的操作装在一个名为LoginPage的page object里:(x)
1 class LoginPage
2 def initialize driver
3 @driver = driver
4 end
5
6 def login_as user
7 @driver.type 'id=
', user[:name]
8 @driver.type 'xpath=
', user[:password]
9 @driver.click 'name=
'
10 @driver.wait_for_page_to_load
11 end
12 end
login_as是一个具有业务含义的面行ؓ(f)。在login_asҎ(gu)中,page object负责通过依靠idQxpathQname{信息完成登录操作。在试中,我们可以q样来用这个page objectQ?br>
1 page = LoginPage.new $selenium
2 page.login_as :name => 'xxx', :password => 'xxx'
3
不过既然用了(jin)rubyQ总要用一些ruby sugar吧,我们定义一个onҎ(gu)来表N面操作的环境Q?br>
1 def on page_type, &block
2 page = page_type.new $selenium
3 page.instance_eval &block if block_given?
4 end
之后我们可以用page object的类名常量和block描述在某个特定页面上操作?jin)?x)
1 on LoginPage do
2 login_as :name => 'xxx', :password => 'xxx'
3 end
4
除了(jin)行ؓ(f)Ҏ(gu)之外Q我们还需要在page object上定义一些获取页面信息的Ҏ(gu)Q比如获取登录页面的Ƣ迎词的Ҏ(gu)Q?br>
def welcome_message
@driver.get_text 'xpath=
'
end
q样试也可表达得更生动一些:(x)
1 on LoginPage do
2 assert_equal 'Welcome!', welcome_message
3 login_as :name => 'xxx', :password => 'xxx'
4 end
当你把所有的面都用Page Object装?jin)之后,有效地分离了(jin)测试和面l构的耦合。在试中,只需使用诸如login_as, add_product_to_cartq样的业务行为,而不必依靠像idQnameq些具体且易变的面元素?jin)。当q些面元素发生变化Ӟ只需修改相应的page object可以了(jin)Q而原有测试基本不需要太大或太多的改动?br>
2. Assertation
只有行ؓ(f)q够不成试Q我们还要判断行为结果,q进行一些断a。简单回一下上面的例子Q会(x)发现q有一些很重要的问题没有解冻I(x)我怎么判断d成功?jin)呢Q我如何才能知道真的是处在登录页面了(jin)呢?如果我调用下面的代码?x)怎样呢?
1 $selenium.open url_of_any_page_but_not_login
2 on LoginPage {
}
因此我们q需要向page object增加一些断a性方法。至,每个面都应该有一个方法用于判断是否真正地辑ֈ?jin)这个页面,如果不处在这个页面中的话Q就不能q行M的业务行为。下面修改LoginPage使之包含q样一个方法:(x)
1 LoginPage.class_eval do
2 include Test::Unit::Asseration
3 def visible?
4 @driver.is_text_present(
) && @driver.get_location == 
5 end
6 end
在visible?Ҏ(gu)中,我们通过对一些特定的面元素Q比如URL地址Q特定的UIl构或元素)(j)q行判断Q从而可以得之是否真正地处在某个面上。而我们目前表达测试的基本l构是由onҎ(gu)来完成,我们也就理成章地在onҎ(gu)中增加一个断aQ来判断是否真的处在某个面上,如果不处在这个页面则不进行Q何的业务操作Q?br>
1 def on page_type, &block
2 page = page_type.new $selenium
3 assert page.visible?, "not on #{page_type}"
4 page.instance_eval &block if block_given?
5 page
6 end
7
q个Ҏ(gu)秘地返回了(jin)page对象Q这里是一个比较tricky的技巧。实际上Q我们只惛_用page != nilq个事实来断a面的流转,比如Q下面的代码描述d成功的页面流转过E:(x)
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name => 'xxx', :password => 'xxx'
end
assert on WelcomeRegisteredUserPage
除了(jin)q个基本断言之外Q我们还可以定义一些业务相关的断言Q比如在购物车页面里Q我们可以定义一个判断购物R是否为空的断aQ?br>
1 def cart_empty?
2 @driver.get_text('xpath=
') == 'Shopping Cart(0)'
3 end
需要注意的是,虽然我们在page object里引入了(jin)Test::Unit::Asseration模块Q但是ƈ没有在断aҎ(gu)里用Q何assert*Ҏ(gu)。这是因为,概念上来讲page objectq不是测试。之包含一些真正的断言Q一则概忉|乱,二则Ҏ(gu)使page object变成针对某些场景的test helperQ不利于以后试的维护,因此我们往往們于将断言Ҏ(gu)实现Z个普通的q回gؓ(f)boolean的方法?br>
3. Test Data
试意图的体C仅仅是在行ؓ(f)的描qCQ同栯有测试数据,比如如下两段代码Q?br>
1 on LoginPage do
2 login_as :name => 'userA', :password => 'password'
3 end
4 assert on WelcomeRegisteredUserPage
5
6 registered_user = {:name => 'userA', :password => 'password'}
7 on LoginPage do
8 login_as registered_user
9 end
10 assert on WelcomeRegisteredUserPage
试的是同一个东西,但是昄W二个测试更好的体现?jin)测试意图?x)使用一个已注册的用L(fng)录,应该q入Ƣ迎面。我们看q个试的时候,往往不会(x)兛_(j)用户名啊密码啊具体是什么,我们兛_(j)它们表达?jin)怎样的测试案例。我们可以通过DataFixture来实现这一点:(x)
1 module DataFixture
2 USER_A = {:name => 'userA', :password => 'password'}
3 USER_B = {:name => 'userB', :password => 'password'}
4
5 def get_user identifier
6 case identifier
7 when :registered then return USER_A
8 when :not_registered then return USER_B
9 end
10 end
11 end
在这里,我们测试案例和具体数据做了(jin)一个对应:(x)userA是注册过的用P而userB是没注册的用戗当有一天,我们需要将d用户名改为邮q时候,只需要修改DataFixture模块可以了(jin)Q而不必修改相应的试Q?br>
1 include DataFixtureDat
2
3 user = get_user :registered
4 on LoginPage do
5 login_as user
6 end
7 assert on WelcomeRegisteredUserPage
当然Q在更复杂的试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这L(fng)对应Q但是M的目的就是ɋ试和测试数据有效性的耦合分离Q?br>
1 def get_user identifier
2 case identifier
3 when :registered then return User.find '
.'
4 end
5 end
4.Navigator
与界面元素类|URL也是一cL变(sh)难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data怼Q这里就不赘qC(jin)Q下面是一个例子:(x)
1 navigate_to detail_page_for @product
2 on ProductDetailPage do
3
.
4 end
5. Shortcut
前面我们已经有了(jin)一个很好的基础Q将Selenium试与各U脆׃意图不明的元素分d?jin),那么最后shortcut不过是在蛋糕上面最漂亮的奶油Ş?jin)——定义具有漂亮语法的helperQ?br>
1 def should_login_successfully user
2 on LoginPage do
3 assert_equal 'Welcome!', welcome_message
4 login_as user
5 end
6 assert on WelcomeRegisteredUserPage
7 end
然后是另外一个magicҎ(gu)Q?br>
1 def given identifer
2 words = identifier.to_s.split '_'
3 eval "get_#{words.last} :#{words[0..-2].join '_'}"
4 end
之前的测试就可以被改写ؓ(f)Q?br>
def test_should_xxxx
should_login_successfully given :registered_user
end
q是一U结论性的shortcut描述Q我们还可以有更behaviour的写法:(x)
1 def login_on page_type
2 on page_type do
3 assert_equal 'Welcome!', welcome_message
4 login_as @user
5 end
6 end
7
8 def login_successfully
9 on WelcomeRegisteredUserPage
10 end
11
12 def given identifer
13 words = identifier.to_s.split '_'
14 eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"
15 end
最后,试׃(x)变成cM验收条g的样子:(x)
1 def test_should_xxx
2 given :registered_user
3 login_on LoginPage
4 assert login_successfully
5 end
Mshortcut是一个无兛_坏,只关乎想象力的东西,情挥洒Ruby DSL?D
l论
Selenium是一个让人又爱又恨的东西Q错误地使用Selenium?x)给整个敏捷团队的开发节奏带来灾难性的影响。不q值得?jin)幸的是正确C用Selenium的原则也是相当的单:(x)
1.通过脆弱易变的面元素和测试分dQ得页面的变化不会(x)Ҏ(gu)试生太大的影响?br>2.明确指定试数据的意图,不在试用用Q何具体的数据?br>3.一切可能,明确地表辑և试的意图,使测试易于理解?br>
当然Q除?jin)遵循这几个基本原则之外Q用page object或其他domain based web testing技术是个不错的选择。它们将?x)帮助你更容易地控制Selenium试的规模,更好地^衡覆盖率和执行效率,从而更加有效地交付高质量的Web目?br>
鸣谢
此文中涉?qing)的都是我最q三周以来对Selenium试q行重构时所采用的真实技术。感谢Nick Drew帮助我清晰地划分?jin)Driver, Page, Nagivator和Shortcut的层ơ关p,它们构成我整个实늚基石Q感谢Chris LeishmanQ在和他pairing programming的过E中Q他帮助我锤g(jin)Ruby DSLQ还有Mark Ryall和AbhiQ是他们W一ơ在目中引入了(jin)Test Data FixtureQ得所有h的工作都变得单v来?br>

]]>