靠着 51testing 论坛管理员“风在吹”同志和一位不知名MM的辛勤工作,我的 blog 终于完成 CNBlogs 和 51testing-blog 的同步。亲爱的同志,我要请你们吃饭 ^_^

应用Selenium和Ruby进行面向领域的Web测试

上一篇 / 下一篇  2007-07-03 16:04:53 / 个人分类:16.开源技术 & 开源文化

查看( 2270 ) / 评论( 3 )
这是一篇非常非常好的文章,其中提出的自动化测试的思想让我对自己以往所设计的自动化测试框架产生了新的想法,并发现可以进一步的改进来提高自动化测试脚本和框架的可读性、可靠性和可维护性。这些思想不仅仅适用于 Selenium 的自动化测试,更可以供其他所有的自动化测试框架所参考。—— Jackei 注
H+E;p}+I"W/RnC [#@051Testing软件测试网o0whCBS!C*J
51Testing软件测试网TpO;~ `
51Testing软件测试网z9Db(Dh

5U$[B8Z!~?*u0作者徐昊发布于 2007年5月21日 下午10时46分
$pGW.Bw0社区Agile,Ruby主题开放源代码,单元测试,Ruby on Rails应用Selenium进行Web测试时,经常会遇到下面的几个麻烦问题:
b;A g*u aB0
  1. 大量使用name、id、xpath等页面元素。无论是功能修改、UI重构还是交互性改进都会影响到这些元素,这使得Selenium测试变得非常脆弱。
  2. 过于细节的页面操作不容易体现出行为的意图,一段时间之后就很难真正把握测试原有的目的了,这使得Selenium测试变得难于维护。
  3. 对具体数据取值的存在依赖,当个别数据不再合法的时候,测试就会失败,但这样的失败并不能标识功能的缺失,这使得Selenium测试变得脆弱且难以维护。
    而这几点直接衍生的结果就是不断地添加新的测试,而极少地去重构、利用原有测试。其实这倒也是正常,单元测试测试写多了,也有会有这样的问题。不过比较要命的是,Selenium的执行速度比较慢(相对单元测试),随着测试逐渐的增多,运行时间会逐渐增加到不可忍受的程度。一组意图不明而且难以维护的Selenium测试,可以很轻松地在每次构建(Build)的时候杀掉40分钟甚至2个小时的时间,我就有曾有花2个小时坐在电脑前面等待450个 Selenium测试运行通过的悲惨经历。因此合理有效地规划Selenium测试就显得格外的迫切和重要了。而目前比较行之有效的办法,往大了说,可以叫基于领域的Web测试(Domain Based Web Testing),具体来讲,就是Page Object Pattern。
    5@'sD~ ?K9C_P0Page Object Pattern里有四个基本概念:Driver、Page、Navigator和Shortcut等。Driver是测试真正的实现机制,比如 Selenium,比如Watir,比如HttpUnit。它们懂得如何去真正执行一个Web行为,通常包含像Click、Select、Type等这样的表示具体行为的方法;Page是对一个具体页面的封装,它们了解页面的结构,知道诸如id、name、class和xpath这类实现细节,并描述用户可以在其上进行何种操作;Navigator则代表了URL,表示一些不经页面操作的直接跳转;最后Shortcut就是helper方法了,需要看具体的需要而定。下面来看一个超级简单的例子——测试登录页面。
    1C's9S-Z.m!LhS01. Page Object假设我们使用一个单独的登录页面进行登录,那么可能会将登录的操作封装在一个名为LoginPage的page object里:51Testing软件测试网!V6A1CB ?gU
    classLoginPage
    N f j:U8[ tuJGm.q8?0  
    definitialize driver
    @0S1hh;[0    @driver
    =driver51Testing软件测试网gC*@ xe-a~7Hd
      end51Testing软件测试网gSh,b/xtw
      
    )x-p&{`f0  
    deflogin_as user
    2DiO&L(o A/A"X ]{0    @driver.type
    'id=', user[:name]
    5BS)M;[j0    @driver.type
    'xpath=', user[:password]
    ta k"Z&_ qQ8H*Q0    @driver.click
    'name='51Testing软件测试网8sKR$A|&qR H:j
        @driver.wait_for_page_to_load
    q?p Qo0  end
    3a[vB)L V0end

    [m1H6{\&qm^051Testing软件测试网*]qb3~0w.`.F1J
    login_as是一个具有业务含义的页面行为。在login_as方法中,page object负责通过依靠id、xpath、name等信息完成登录操作。在测试中,我们可以这样来使用这个page object:51Testing软件测试网o!c*I:b X ^k!@K
    page=LoginPage.new $selenium51Testing软件测试网O%tZ1IYu'^(rRn
    page.login_as :name
    =>
    /iBmJMd0'xxx', :password=>
    0rg3qKOAZQ0'xxx'
    51Testing软件测试网B$q:@-q/G$`1q*t gXl$v3S

    v:};N/Bt?O.q%F2x0不过既然用了Ruby,总要用一些ruby sugar吧,我们定义一个on方法来表达页面操作的环境:51Testing软件测试网~m D U/ebB$a
    defon page_type,&block51Testing软件测试网;i5f&r(p.MYR
      page
    =page_type.new $selenium51Testing软件测试网"`#tS1j4N,in
      page.instance_eval
    &blockifblock_given?
    {T mrW"^s@Lx0end

    ,s*H`:W F7HkUR'o6B0
    E8\*M3Na'?Z0之后我们就可以使用page object的类名常量和block描述在某个特定页面上操作了:
    6u.R-d}EV1m0on LoginPage do51Testing软件测试网PmY|9_K Hq.u
      login_as :name
    =>51Testing软件测试网/O$i)k b#`Ejd C
    'xxx', :password=>51Testing软件测试网'~.F9pO{!|
    'xxx'51Testing软件测试网$_#U9P2JXN
    end
    51Testing软件测试网S)I2g;X7K+o.U q3?C

    I)la2tAY,pK0除了行为方法之外,我们还需要在page object上定义一些获取页面信息的方法,比如获取登录页面的欢迎词的方法:
    "wz?7Q/I }:d0defwelcome_message51Testing软件测试网J W7nBX!o@ g1}B\
      @driver.get_text
    'xpath='51Testing软件测试网3i&o&So*nU;[
    end
    51Testing软件测试网|Iw-q#]e4v
    51Testing软件测试网4B+v,`m a$kg
    这样测试也可表达得更生动一些:51Testing软件测试网,}(o:})zxM?t
    on LoginPage do
    GSx6P2I*Uup/]0  assert_equal
    'Welcome!', welcome_message51Testing软件测试网Z w[0t/x;lq [3u ^
      login_as :name
    =>
    8{ v.SrS,gn8Jn;A0'xxx', :password=>51Testing软件测试网2m E+r8s0lt
    'xxx'51Testing软件测试网3AO"oX"k,V y4a
    end

    Y/_0TzF.N0
    }D0uh|0当你把所有的页面都用Page Object封装了之后,就有效地分离了测试和页面结构的耦合。在测试中,只需使用诸如login_as和add_product_to_cart这样的业务行为,而不必依靠像id、name等这些具体且易变的页面元素了。当这些页面元素发生变化时,只需修改相应的page object就可以了,而原有测试基本不需要太大或太多的改动。51Testing软件测试网l;sH-Z6{'rl
    2. Assertation只有行为还构不成测试,我们还要判断行为结果,并进行一些断言。简单回顾一下上面的例子,会发现还有一些很重要的问题没有解决:我怎么判断登录成功了呢?我如何才能知道真的是处在登录页面了呢?如果我调用下面的代码会怎样呢?
    C$K ^G#~ H+O'|0$selenium.open url_of_any_page_but_not_login51Testing软件测试网"p d6v L5U/D
    on LoginPage {}
    51Testing软件测试网{ Y(h8x%L3yJY!V

    S-I'H:{Y0因此我们还需要向page object增加一些断言性方法。至少,每个页面都应该有一个方法用于判断是否真正地达到了这个页面,如果不处在这个页面中的话,就不能进行任何的业务行为。下面修改LoginPage使之包含这样一个方法:51Testing软件测试网)W){N@cv3S
    LoginPage.class_eval do
    -N~(U wH0  include Test::Unit::Asseration51Testing软件测试网_(V/j ]4UTp[
      
    defvisible?
    [6yE`2NK&|.W7i$E0    @driver.is_text_present()
    &&@driver.get_location==51Testing软件测试网6j.]5g"yoF}K
      end51Testing软件测试网.SW,kgo+O[4e(p
    end
    51Testing软件测试网F tL_ fe v
    51Testing软件测试网} ?p u&\b)ya%d
    在visible?方法中,我们通过对一些特定的页面元素(比如URL地址,特定的UI结构或元素)进行判断,从而可以得之是否真正地处在某个页面上。而我们目前表达测试的基本结构是由on方法来完成,我们也就顺理成章地在on方法中增加一个断言,来判断是否真的处在某个页面上,如果不处在这个页面则不进行任何的业务操作:
    w[9`'E2?s0defon page_type,&block
    7U1py-v$o FA3^m?0  page
    =page_type.new $selenium
    _)N,x2nI8y,`z X'@0  
    assertpage.visible?,"not on #{page_type}"51Testing软件测试网m7C0ZY)_7|
      page.instance_eval
    &blockifblock_given?51Testing软件测试网3g6P zE i2Y C'E
      page
    2Zu6i uq5ys\0end

    A]/zgi0
    5N+rt$oD0这个方法神秘地返回了page对象,这里是一个比较取巧的技巧。实际上,我们只想利用page != nil这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:51Testing软件测试网v3s4i~ D4s^n7R
    on LoginPage do
    F-oozO]0  assert_equal
    'Welcome!', welcome_message
    r(K%N,J1Ddbd"k0  login_as :name
    =>
    _b*m[,F|.]M0'xxx', :password=>51Testing软件测试网s:m5UF!Q
    'xxx'51Testing软件测试网&} | Q{3^"BPw`!S
    end
    1sLALB-n&z0
    asserton WelcomeRegisteredUserPage
    51Testing软件测试网c+P c4n1T~g
    51Testing软件测试网*h XVE6c
    除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:
    :L\&kZqz*V0defcart_empty?51Testing软件测试网a^D mB.nZ]
      @driver.get_text(
    'xpath=')==51Testing软件测试网;]P3A#am*gi#u+m
    'Shopping Cart(0)'
    |8V ^9fR;?R-bFS0end

    d1dzu'R;]?9r0
    2CX` [ Z&oC0需要注意的是,虽然我们在page object里引入了Test::Unit::Asseration模块,但是并没有在断言方法里使用任何assert*方法。这是因为,概念上来讲 page object并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使page object变成针对某些场景的test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为boolean的方法。
    %e^%pgq/b#m03. Test Data测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:
    8wz S%L(BBN0on LoginPage do51Testing软件测试网oBTFc x
      login_as :name
    =>
    -I@ EA(m V$` Oc:e0'userA', :password=>
    w8o$bD0d2A0'password'
    `dmUC,C)pDA*n0end
    y8fd7UG5k0
    asserton WelcomeRegisteredUserPage
    _(Ri}NfS:q051Testing软件测试网|1W]:\OTi,e| B
    registered_user
    ={:name=>51Testing软件测试网'W!IR#}^-kdk$V
    'userA', :password=>51Testing软件测试网wq2WR2`Ml%K%E Ba1mX
    'password'}
    q U)C%q9Al ] w0on LoginPage do51Testing软件测试网)wR4mzlx2`#P5JFj
      login_as registered_user51Testing软件测试网aEzMz_
    end51Testing软件测试网8u3`o? _(m5J&RAw
    asserton WelcomeRegisteredUserPage

    ){5{ KL:s\:H EZ051Testing软件测试网T'W+aSM'ep
    测试的是同一个东西,但是显然第二个测试更好的体现了测试意图:使用一个已注册的用户登录,应该进入欢迎页面。我们看这个测试的时候,往往不会关心用户名啊密码啊具体是什么,我们关心它们表达了怎样的测试案例。我们可以通过DataFixture来实现这一点:
    &}0}a&Xd1i9s&t0module DataFixture
    p6_0Ot#YH0  USER_A
    ={:name=>51Testing软件测试网K.o5FPE5sUP3y~
    'userA', :password=>
    meofhNs2X0'password'}51Testing软件测试网"vc?uS Jr.a
      USER_B
    ={:name=>
    %x hm6FO7Ad8K0'userB', :password=>
    e6Z:{'y)xr:BkV d1S H,m0'password'}
    s-c.GE.E5y#S051Testing软件测试网+uT'bN"C
      
    defget_user identifier51Testing软件测试网m"WS }a\ G
        case identifier
    hy(F~3k qs%xO)j0    when :registered then
    returnUSER_A51Testing软件测试网 n!WO4N0uT4o"{3p'}
        when :not_registered then
    returnUSER_B51Testing软件测试网]-Q&b x1ljKV3C0y^
        end51Testing软件测试网+FW/dRb@V
      end
    Kf y"L^F0end
    51Testing软件测试网6BIAC,L2OOw
    51Testing软件测试网 aSW%Z flI J
    在这里,我们将测试案例和具体数据做了一个对应:userA是注册过的用户,而userB是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改DataFixture模块就可以了,而不必修改相应的测试:51Testing软件测试网/o}!m|"Nd
    include DataFixtureDat
    Wr}KMqY051Testing软件测试网hd#C2W o@B
    user
    =get_user :registered
    3eBJ3P&u$D"q%Pj0on LoginPage do51Testing软件测试网&G2X&O\8F_P;a
      login_as user51Testing软件测试网N m)WVyK6Cjn
    end51Testing软件测试网1f,E-~yz.b'U
    asserton WelcomeRegisteredUserPage

    !o4|;f*D;N051Testing软件测试网1__(M^w c-D
    当然,在更复杂的测试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:51Testing软件测试网2Pv7q.['D
    defget_user identifier
    '[8~'b~'P$Co2h9O0  case identifier51Testing软件测试网5z`-|U jg2b5y5bB
      when :registered then
    returnUser.find'.'  
    9t oBmKE ~*{ `#x0  end
    Z qS2tr[b;m{3ay0end

    $w^-f)z Kl6~(?0
    N~ x ^a04.Navigator与界面元素类似,URL也是一类易变且难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data相似,这里就不赘述了,下面是一个例子:
    q/nHx8j0el"g-^0navigate_to detail_page_for @product
    ^c2|9K.x!s\4g9d^"~0on ProductDetailPage do
    WZJ,c ~z0  .
    g'\ii'W`6x JZ[0end

    ;e_"P1T*Vu051Testing软件测试网r ` M`VL_Wg
    5. Shortcut前面我们已经有了一个很好的基础,将Selenium测试与各种脆弱且意图不明的元素分离开了,那么最后shortcut不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的helper:51Testing软件测试网a_ [O8t
    defshould_login_successfully user
    a^ g m+TA,z+b0  on LoginPage do51Testing软件测试网6N'}0H8`%KR$@ ]+_H
        assert_equal
    'Welcome!', welcome_message51Testing软件测试网!E%Tly,Y
        login_as user
    )vLk j&?2R0~1r"NNG0  end
    vf:ATESS0  
    asserton WelcomeRegisteredUserPage51Testing软件测试网3X,L2TXD f1i:^h$XK#X
    end

    +?r6L(H`h fX051Testing软件测试网,c gi[B*K9Hx
    然后是另外一个magic方法:
    x T4^L!^0}0defgiven identifer51Testing软件测试网-HT#M!X+L#us
      words
    =identifier.to_s.split'_'51Testing软件测试网/pr}7G5MwD ~ _
      eval
    "get_#{words.last} :#{words[0..-2].join '_'}"
    :l&yO5?f~.V:{*J0end

    ,O0JV2WRLN0
    _C"H;zCG pI0之前的测试就可以被改写为:51Testing软件测试网R1U}1PFG$y:w_
    deftest_should_xxxx51Testing软件测试网 n:^JIo H3it*j
      should_login_successfully given :registered_user
    U9^5xy+V0end

    b hb'~tr0
    W_o:l.sA.OC0这是一种结论性的shortcut描述,我们还可以有更behaviour的写法:51Testing软件测试网Sl"A.Oov
    deflogin_on page_type
    "W&]O;]!t_$om C7P0  on page_type do
    o,cQ(q.mLH kF&sV,Fs0    assert_equal
    'Welcome!', welcome_message51Testing软件测试网1w&a!I n(l[S
        login_as @user51Testing软件测试网B%A:S;`v$ON Zn9?
      end51Testing软件测试网gv,M;J"fF
    end
    z.a*Av+R2\:de*L051Testing软件测试网%I3B c9L2[+t
    deflogin_successfully51Testing软件测试网 | P S0\T5k9{Q
      on WelcomeRegisteredUserPage51Testing软件测试网j8H,U3u}Yx
    end
    a UN-RC[051Testing软件测试网Y6A:^&A\){s
    defgiven identifer51Testing软件测试网TP,K5vk0U
      words
    =identifier.to_s.split'_'51Testing软件测试网,|n#d|G,Ux h
      eval
    "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"
    5c}-ed}Lx9A0end

    4p`(?|'}1R051Testing软件测试网E;lOzg&i7s(p
    最后,测试就会变成类似验收条件的样子:
    R+o'b8gAS0deftest_should_xxx51Testing软件测试网1y$G0] x ^9`Ov
      given :registered_user51Testing软件测试网T't$s*T3s\/_
      login_on LoginPage
    GPzM-V:E0  
    assertlogin_successfully
    ,@/tmyA0end

    ?liN*FU0
    6MIW0~3Dk0总之shortcut是一个无关好坏,只关乎想象力的东西,尽情挥洒Ruby DSL吧!51Testing软件测试网A:Rf0r{"T7n
    结论Selenium是一个让人又爱又恨的东西,错误地使用Selenium会给整个敏捷团队的开发节奏带来灾难性的影响。不过值得庆幸的是正确地使用Selenium的原则也是相当的简单:
    ?9X*H1\ @!xZV2l0
    1. 通过将脆弱易变的页面元素和测试分离开,使得页面的变化不会对测试产生太大的影响。
    2. 明确指定测试数据的意图,不在测试用使用任何具体的数据。
    3. 尽一切可能,明确地表达出测试的意图,使测试易于理解。
      当然,除了遵循这几个基本原则之外,使用page object或其他domain based web testing技术是个不错的选择。它们将会帮助你更容易地控制Selenium测试的规模,更好地平衡覆盖率和执行效率,从而更加有效地交付高质量的Web项目。
      ZX`-{0HID0此文中涉及的都是我最近几周以来对Selenium测试进行重构时所采用的真实技术。感谢Nick Drew帮助我清晰地划分了Driver、 Page、Nagivator和Shortcut的层次关系,它们构成我整个实践的基石;感谢Chris Leishman,在和他结对编程的过程中,他帮助我锤炼了Ruby DSL;还有Mark Ryall和Abhi,是他们第一次在项目中引入了Test Data Fixture,使得所有人的工作都变得简单起来。51Testing软件测试网+p?l)W ]z
      作者简介:徐昊,ThoughtWorks咨询师和敏捷过程教练;BJUG(Beijing Java User Group)和AgileChina主要创始人之一;RSSer(Ruby,Smalltalk & Scheme)。目前主要致力于研究编译理论和推广DSL(Domain Specified Language)在实际项目中的应用。他的博客地址是:http://www.blogjava.net/raimundox

      TAG:

      Jackei 的测试生活与人文社会读本 jackei 发布于2007-07-19 15:29:40
      呵呵,看来是曲高和寡啊 ^_^
      测试天地 yuxiao 发布于2007-07-24 11:06:01
      呵呵,我没看懂啊,我会继续努力的
      韩诺发布于2007-09-13 16:26:47
      我也看不懂,继续努力!
      我来说两句

      (可选)

      Open Toolbar