什么是Page Object
Page Object是Selenium中提出的一种测试设计模式。在Web前端自动化测试的过程中,Page Object可以称得上是居家必备的良品之一。PageObject将与Web测试页面交互的行为封装在其内部,旨在将每个页面或者相似页面的功能封装,例如页面中需要测试的元素(按钮,输入框,标题等),这样,通过在测试中访问Page类的相应方法来获取页面相应的元素,从而巧妙的避免了当页面元素id或者位置变化时,需要修改测试代码的情况。
言而总之,总而言之,PageObject就是将Web页面元素的变化封装起来,提供API供外部测试代码调用,从而达到将测试代码于Web页面元素的变化解耦。
Page Object的优点
- 将测试代码与页面元素的操作解耦
- 将Web页面元素的访问统一化、规范化、结构化
- 封装不同浏览器对元素访问的差异
- 易于实现Web元素访问的DSL
对于小规模站点的Web自动化测试,直接使用Capybara之类的框架并不会出现问题。但对于业务复杂,功能完备的大规模Web站点自动化测试而言,如果不使用PageObject将测试代码于Web页面元素的访问解耦,而在测试代码中直接访问Web页面元素,不仅易造成冗余代码,还会导致测试集结构混乱,后期难于理解和维护。
Gizmo
在最近的一个项目中,我们使用Capybara进行Web自动化测试,同时使用Gizmo来完成PageObject的定义。 Gizmo出自REA Group之手,使用Nokogiri对Selenimu的Driver进行轻量级的封装,并定义自己的Page对象。
使用Gizmo时注意的要点:
- PageObject可以是整个页面,也可是页面的某一小块。但都统称为Page Module。
- PageObject被定义成module,而非class
- PageObject的命名为page_with_
, 而调用者则遵循on_page_with : 的模式进行调用 - PageObject使用define_action作为行为的定义,例如 点击button,填充form,输入文本等。
- PageObject使用def selectors作为获取属性的定义,例如获取页面对象,获取文本等。
不过,Gizmo中也存在一些问题:
a) Page对象只对生成时的DOM元素有效。
如果DOM元素随后发生变化(例如Javascript操作DOM), 则无法捕获到。需要显示的重新更新PageObject对DOM元素的引用。
def refresh
@document = Nokogiri::HTML(body)
end
另外,作者貌似已经不再维护Gizmo了,上次的更新要追溯到3年前了。所以如果有不爽的地方,要做好fork&customize的打算。 :(
Capybara-Page-Object(CPO)
了解完Gizmo,让我们来认识下Capybara-Page-Object。
除了包含Gizmo现有的大部分优点之外,CPO对开发者更加友好:
Page与Component共同使用
在CPO中,Page的定义通常是指整个页面。而对于页面的某一块功能或者布局,通常被定义为Component。
譬如,首页作为一个Page对象,可能会包含Login,BasicSearchForm,SearchResult等Component。
而搜索页面,作为Page对象,可能包含Login,BasicSearchForm以及AdvancedSearchForm等Component。
换句话说,一个Page是由多个Component组成,而Component(如果Element类似)则又可以被不同的Page重用。
对HTML元素的封装
在CPO中,实现了对HTML不同元素的分装。如Form,Image,List,ListItem, Select, Input等。
通过使用CPO封装后的HTML元素类,开发者可以更轻松的访问不同类型的HTML元素,而不必为每个心仪元素定义CSS或者Xpath查找策略。
如下是CapybaraPageObject::Form的定义,其中已经提供了对Form内buttons,inputs,selects等内元素的访问接口。
class Form < CapybaraPageObject::Element
def fields
r = ActiveSupport::OrderedHash.new
r.merge! inputs
r.merge! selects
r.merge! textareas
end
def buttons
r = []
all('input').each do |element|
input = Input.new(element)
next unless input.button?
r << element
end
all('button').each do |button|
r << button
end
r
end
def inputs
all('input').each_with_object(ActiveSupport::OrderedHash.new) do |input_tag, hash|
input = Input.new(input_tag)
next if input.button?
if input.checkable?
hash[input.key] = !! input.checked?
else
hash[input.key] = input_tag.value
end
end
end
def textareas
all('textarea').inject(ActiveSupport::OrderedHash.new) do |result, element|
textarea = Textarea.new(element)
result.merge textarea.key => textarea.value
end
end
def selects
all('select').inject(ActiveSupport::OrderedHash.new) do |result, element|
select = Select.new(element)
result.merge select.key => select.value
end
end
end
有了这些封装类,在定义Component时需要做的逻辑就很简单了
module Pages
module Components
class SearchForm < CapybaraPageObject::Form
def search(query)
source.fill_in 'as_q', :with => query
source.click_button 'Search'
end
end
end
end
然后再Page中使用Component时,只需传入Form对应的Selector即可。
module Pages
module Search
class Index < Pages::Base
component(:search_form) do
Components::SearchForm.new find('.advsearch')
end
def search(*args)
search_form.search(*args)
end
end
end
end
关于本篇提到的代码,如有兴趣,请参考我的cucumber-capybara示例