原文:
应用Selenium进行Web测试往往会存在几个bad smell:
1.大量使用name, id, xpath等页面元素。无论是功能修改、UI重构还是交互性改进都会影响到这些元素,这使得Selenium测试变得非常脆弱。
2.过于细节的页面操作不容易体现出行为的意图,一段时间之后就很难真正把握测试原有的目的了,这使得Selenium测试变得难于维护。
3.对具体数据取值的存在依赖,当个别数据不再合法的时候,测试就会失败,但这样的失败并不能标识功能的缺失,这使得Selenium测试变得脆弱且难以维护。
而这几点直接衍生的结果就是不断地添加新的测试,而极少地去重构、利用原有测试。其实这到也是正常,单元测试测试写多了,也有会有这样的问题。不过比较要命的是,Selenium的执行速度比较慢(相对单元测试),随着测试逐渐的增多,运行时间会逐渐增加到不可忍受的程度。一组意图不明难以维护的Selenium测试,可以很轻松地在每次build的时候杀掉40分钟甚至2个小时的时间,在下就有花2个小时坐在电脑前面等待450个Selenium测试运行通过的悲惨经历。因此合理有效地规划Selenium测试就显得格外的迫切和重要了。而目前比较行之有效的办法,往大了说,可以叫domain based web testing,具体来讲,就是Page Object Pattern。
Page Object Pattern里有四个基本概念:Driver, Page, Navigator和Shortcut。Driver是测试真正的实现机制,比如Selenium,比如Watir,比如HttpUnit。它们懂得如何去真正执行一个web行为,通常包含像click,select,type这样的表示具体行为的方法;Page是对一个具体页面的封装,它们了解页面的结构,知道诸如id, name, class,xpath这类实现细节,并描述用户可以在其上进行何种操作;Navigator则代表了URL,表示一些不经页面操作的直接跳转;最后Shortcut就是helper方法了,需要看具体的需要了。下面来看一个超级简单的例子——测试登录页面。
1. Page Object
假设我们使用一个单独的Login Page进行登录,那么我们可能会将登录的操作封装在一个名为LoginPage的page object里:
login_as是一个具有业务含义的页面行为。在login_as方法中,page object负责通过依靠id,xpath,name等信息完成登录操作。在测试中,我们可以这样来使用这个page object:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 page = LoginPage.new $selenium
2 page.login_as :name => 'xxx', :password => 'xxx'
3
不过既然用了ruby,总要用一些ruby sugar吧,我们定义一个on方法来表达页面操作的环境:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><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描述在某个特定页面上操作了:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 on LoginPage do
2 login_as :name => 'xxx', :password => 'xxx'
3 end
4
除了行为方法之外,我们还需要在page object上定义一些获取页面信息的方法,比如获取登录页面的欢迎词的方法:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
def welcome_message
@driver.get_text 'xpath='
end
这样测试也可表达得更生动一些:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 on LoginPage do
2 assert_equal 'Welcome!', welcome_message
3 login_as :name => 'xxx', :password => 'xxx'
4 end
当你把所有的页面都用Page Object封装了之后,就有效地分离了测试和页面结构的耦合。在测试中,只需使用诸如login_as, add_product_to_cart这样的业务行为,而不必依靠像id,name这些具体且易变的页面元素了。当这些页面元素发生变化时,只需修改相应的page object就可以了,而原有测试基本不需要太大或太多的改动。
2. Assertation
只有行为还够不成测试,我们还要判断行为结果,并进行一些断言。简单回顾一下上面的例子,会发现还有一些很重要的问题没有解决:我怎么判断登录成功了呢?我如何才能知道真的是处在登录页面了呢?如果我调用下面的代码会怎样呢?
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
1 $selenium.open url_of_any_page_but_not_login
2 on LoginPage {}
因此我们还需要向page object增加一些断言性方法。至少,每个页面都应该有一个方法用于判断是否真正地达到了这个页面,如果不处在这个页面中的话,就不能进行任何的业务行为。下面修改LoginPage使之包含这样一个方法:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
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?方法中,我们通过对一些特定的页面元素(比如URL地址,特定的UI结构或元素)进行判断,从而可以得之是否真正地处在某个页面上。而我们目前表达测试的基本结构是由on方法来完成,我们也就顺理成章地在on方法中增加一个断言,来判断是否真的处在某个页面上,如果不处在这个页面则不进行任何的业务操作:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><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
这个方法神秘地返回了page对象,这里是一个比较tricky的技巧。实际上,我们只想利用page != nil这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name => 'xxx', :password => 'xxx'
end
assert on WelcomeRegisteredUserPage
除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
1 def cart_empty?
2 @driver.get_text('xpath=') == 'Shopping Cart(0)'
3 end
需要注意的是,虽然我们在page object里引入了Test::Unit::Asseration模块,但是并没有在断言方法里使用任何assert*方法。这是因为,概念上来讲page object并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使page object变成针对某些场景的test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为boolean的方法。
3. Test Data
测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><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
测试的是同一个东西,但是显然第二个测试更好的体现了测试意图:使用一个已注册的用户登录,应该进入欢迎页面。我们看这个测试的时候,往往不会关心用户名啊密码啊具体是什么,我们关心它们表达了怎样的测试案例。我们可以通过DataFixture来实现这一点:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>--> 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
在这里,我们将测试案例和具体数据做了一个对应:userA是注册过的用户,而userB是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改DataFixture模块就可以了,而不必修改相应的测试:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 include DataFixtureDat
2
3 user = get_user :registered
4 on LoginPage do
5 login_as user
6 end
7 assert on WelcomeRegisteredUserPage
当然,在更复杂的测试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
1 def get_user identifier
2 case identifier
3 when :registered then return User.find '.'
4 end
5 end
4.Navigator
与界面元素类似,URL也是一类易变且难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data相似,这里就不赘述了,下面是一个例子:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->
1 navigate_to detail_page_for @product
2 on ProductDetailPage do
3 .
4 end
5. Shortcut
前面我们已经有了一个很好的基础,将Selenium测试与各种脆弱且意图不明的元素分离开了,那么最后shortcut不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的helper:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><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方法:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 def given identifer
2 words = identifier.to_s.split '_'
3 eval "get_#{words.last} :#{words[0..-2].join '_'}"
4 end
之前的测试就可以被改写为:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->def test_should_xxxx
should_login_successfully given :registered_user
end
这是一种结论性的shortcut描述,我们还可以有更behaviour的写法:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>--> 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
最后,测试就会变成类似验收条件的样子:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->1 def test_should_xxx
2 given :registered_user
3 login_on LoginPage
4 assert login_successfully
5 end
总之shortcut是一个无关好坏,只关乎想象力的东西,尽情挥洒Ruby DSL吧:D
结论
Selenium是一个让人又爱又恨的东西,错误地使用Selenium会给整个敏捷团队的开发节奏带来灾难性的影响。不过值得庆幸的是正确地使用Selenium的原则也是相当的简单:
1.通过将脆弱易变的页面元素和测试分离开,使得页面的变化不会对测试产生太大的影响。
2.明确指定测试数据的意图,不在测试用使用任何具体的数据。
3.尽一切可能,明确地表达出测试的意图,使测试易于理解。
当然,除了遵循这几个基本原则之外,使用page object或其他domain based web testing技术是个不错的选择。它们将会帮助你更容易地控制Selenium测试的规模,更好地平衡覆盖率和执行效率,从而更加有效地交付高质量的Web项目。
鸣谢
此文中涉及的都是我最近三周以来对Selenium测试进行重构时所采用的真实技术。感谢Nick Drew帮助我清晰地划分了Driver, Page, Nagivator和Shortcut的层次关系,它们构成我整个实践的基石;感谢Chris Leishman,在和他pairing programming的过程中,他帮助我锤炼了Ruby DSL;还有Mark Ryall和Abhi,是他们第一次在项目中引入了Test Data Fixture,使得所有人的工作都变得简单起来。
分享到:
相关推荐
Page Object模式是自动化测试中的一种最佳实践,尤其在Python结合Selenium进行网页自动化测试时,它能显著提高代码的可维护性和可复用性。在Page Object模式中,每个网页被视为一个独立的类,这个类封装了页面上的...
PageObject模式是Selenium中推荐的一种最佳实践,旨在提高测试代码的可维护性和可读性。本篇将深入探讨如何利用PageObject模式对登录页面进行测试。 PageObject模式的核心思想是将Web页面看作一个独立的对象,每个...
PageObject是一种设计模式,常用于自动化测试,特别是Web应用程序的Selenium测试中。该模式的主要目的是提高代码的可维护性和可重用性,通过将页面元素和与它们相关的操作封装成独立的对象,来抽象出页面的逻辑。在...
适合初级的测试开发 想要了解如何使用selenium和python编写ui自动化 并使用了Pageobject模式,和python测试框架 pytest 其中pageObject+pytest学习是用chatgpt4.0生成的。
Selenium Page Object Generator是改善您的工作流程的必不可少的工具。 只要配置了所有选项和模板,它就会在活动的或选项卡上单击生成。 生成的页面对象模型将保存到预配置的Chrome或Opera下载文件夹中。 通过从...
selenium Page Object设计模式与实战
总的来说,"PageObject.zip"中的项目是一个基于Python的自动化测试框架,它利用unittest进行测试组织,selenium进行Web交互,PageObject模式提升代码结构,同时具备了根据tag选择运行测试用例的功能,以及将测试结果...
Python Selenium PageObject模式实现 在自动化测试领域,Selenium是一个广泛应用的工具,它允许开发者用各种编程语言(如Python)来控制浏览器的行为。PageObject模式是Selenium测试中的一种最佳实践,它帮助我们...
not only page object. 一个关于 Page Object 模型(PO模型,POM)的包,基于 Selenium,帮助你在 Web 测试中构建 POM。
在Selenium自动化测试中,PageObject设计模式是一种广泛采用的最佳实践,它有助于提高代码的可维护性和可重用性。PageObject模式的核心理念是将Web页面看作一个独立的对象,每个页面都有自己的类,其中包含了该页面...
在本项目中,我们重点关注的是Page Object(PO)模式,这是一种设计模式,用于组织和管理自动化测试代码,使其更易于维护和扩展。 PO模式的核心思想是将Web页面的UI元素和与其相关的业务逻辑封装成一个独立的类,每...
【标题】"selenium服务端客户端以及chrome和firefox的驱动" 在自动化测试领域,Selenium是一个广泛使用的开源工具,用于模拟真实用户与Web浏览器的交互。这个压缩包包含的四个关键文件是Selenium服务器、Selenium...
基于 python 的 selenium UI 自动化测试框架,采用 Page Object 设计模式进行二次开发.zip 1、该资源内项目代码经过严格调试,下载即用确保可以运行! 2、该资源适合计算机相关专业(如计科、人工智能、大数据、数学...
selenium提升第五课-Page Object.pptx 和 13-Page Object.pptx 应该包含了Page Object模式的详细讲解和实践案例。 4. 测试流程和最佳实践:04.pptx、03.pptx、05.pptx、092.pptx、09.pptx 可能涵盖了Selenium测试的...
selenium selenium selenium selenium selenium selenium selenium selenium selenium selenium selenium selenium selenium selenium
python selenium page 类封装
Selenium 的主要优势在于其能够模拟真实用户的浏览器行为,这使得它在功能测试、回归测试以及界面测试方面表现卓越。 #### 二、Selenium 的版本 Selenium 有两个主要版本:Selenium-Core 和 Selenium-RC(Selenium...
Use the PageFactory pattern to build a robust and easy to maintain test framework Build data-driven tests and extend Selenium API to implement custom steps and checks Integrate and use ATDD/BDD tools ...