InfoQ

InfoQ

文章

我的书签

登录注册 以永久保存书签。

该内容已经被标记书签!

标记书签错误,请重试!

使用iTest2重构自动化功能测试脚本

作者 Zhimin Zhan 译者 金明 发布于 2009年8月31日

领域
架构 & 设计,
过程 & 实践,
语言 & 开发
主题
软件测试 ,
敏捷
标签
测试

介绍

众所周知,自动测试脚本很难维护。随着敏捷方法学在企业软件项目中的广泛应用,其核心实践之一——自动化功能测试已经证明了它的价值,同时却也对项目提出了挑战。传统的“录制-回播”类型的测试工具也许能帮助测试人员很快地创建一系列的测试脚本,但这些测试代码最后却很难维护。原因就是:应用程序在不断变化。

在编程的世界中,“重构”(在不影响软件外在行为的前提下,改善软件内部结构的一种方法)已经成为程序员之间频繁使用的词汇。简而言之,通过重构,程序员让代码变得更易于理解、设计也更灵活。经验丰富的敏捷项目经理会给程序员分配一定的时间来重构代码,或者把重构作为完成用户故事的一部分。大部分的集成开发环境(IDE)已经对多种重构方式提供了内置支持。

开发或者维护自动测试脚本的测试人员就没有这份惬意了,虽然他们也有使自动测试脚本变得可读和可维护的要求。软件发布新版本,会伴随新特性、bug修复和软件变更,要想跟踪与之对应的测试脚本,这很难(而且,测试脚本越多,这项工作就越困难)。

测试重构

对功能测试的重构目标和流程与代码重构一样,但有自己的特点:

  • 目标受众
    测试工具的最终用户包括测试人员、业务分析师,甚至还有客户。事实是测试人员、业务分析师和客户一般都不掌握编程技能,整个范式因此而改变。
  • 脚本语法
    代码重构主要是在编译型语言(比如Java和C#)上得到支持。函数式测试脚本,可能是XML、厂商专有脚本、编译型语言或者脚本语言(比如Ruby)。根据测试框架不同,重构的使用形式也不同。
  • 功能测试专属重构
    很多通用的代码重构技巧,比如“重命名”,可以用在功能测试脚本里面,它们特定于测试意图,比如“Move the scripts to run each test case”。

iTest2 IDE

iTest2 IDE是一款新的功能测试工具,专为测试人员设计,让他们能够很轻松地开发和维护自动测试脚本。iTest2完全致力于web测试的自动化,它支持的测试框架是使用RSpec语法的rWebUnit(是广为流行的Watir的一款开源插件)。

iTest2背后的哲学是:容易、简单。试用显示:没有编程经验的测试人员在指导下,平均只需要少于10分钟的时间就能编写他们第一个自动化测试脚本。借助于iTest2,测试人员可以开发、维护和验证功能需求的测试脚本;开发人员可以验证特性可用;业务分析师/客户通过查看测试运行结果(在真实的浏览器下,比如IE或者Firefox)来验证功能需求。

由iTest2创建的测试脚本可以从命令行运行,也能集成在持续构建服务器上。

演练

事实胜于雄辩。下面我们就来看看如何使用iTest2提供的重构工具创建两个测试用例,使它们变得更易理解和维护。

测试计划

为了练习,我们给Mecury's NewTour网站开发了一些典型但是简单的web测试脚本。

站点URL http://newtours.demoaut.com
测试数据: 用户登录:agileway / agileway
测试用例001: 一个注册客户可以选择单程航行方式,从纽约前往悉尼。
测试用例002: 一个注册客户可以选择往返方式,从纽约前往悉尼。

 

自动化测试  
测试脚本框架: rWebUnit(开源的Watir扩展)
测试执行方法: 通过命令行或iTest2 IDE
测试编辑器/工具: iTest2 IDE

创建测试用例001

1. 创建项目

首先,我们创建一个iTest2项目,指定网站URL。一个简单的测试脚本文件就会被创建出来,如下所示:

load File.dirname(__FILE__) + '/test_helper.rb'

test_suite "TODO" do
  include TestHelper

  before(:all) do
    open_browser "http://newtours.demoaut.com"
  end

  test "your test case name" do
    # add your test scripts here
  end
end

2. 使用iTest2Recorder录制测试用例001的测试脚本

我们使用iTest2Recorder,这是Firefox的一个插件,能录制用户在Firefox浏览器中的操作,并记录为可执行的测试脚本。

enter_text("userName", "agileway")
enter_text("password", "agileway")
click_button_with_image("btn_signin.gif")
click_radio_option("tripType", "oneway")
select_option("fromPort", "New York")
select_option("toPort", "Sydney")
click_button_with_image("continue.gif")
assert_text_present("New York to Sydney")

3. 把录好的测试脚本贴到一个测试脚本文件里面,运行

# ...
test "[001] one way trip" do
  enter_text("userName", "agileway")
  enter_text("password", "agileway")
  click_button_with_image("btn_signin.gif")
  click_radio_option("tripType", "oneway")
  select_option("fromPort", "New York")
  select_option("toPort", "Sydney")
  click_button_with_image("continue.gif")
  assert_text_present("New York to Sydney")
end

现在运行测试用例(右键单击,然后选择“Run [001] one way trip”),它通过了!

使用Page对象进行重构

上面的测试脚本可以工作,而且rWebUnit语法也非常易读。有人可能对重构的要求提出质疑,也许还会问“使用Page”是怎么回事?

首先,以现在的格式来看,测试脚本并不易于维护。假设我们已经有了数百个自动测试脚本,而新发布的软件修改了用户认证方式,使用客户邮箱作为用户名登录,这意味着我们需要在测试脚本里面使用‘email’,而不再是‘userName’。在数百个文件里面查找替换,那可不是个好主意。况且,项目成员也喜欢使用项目里面的通用词汇,有一个很美妙的名字来称呼它们:领域专属语言(DSL)。在测试脚本里面也使用这些词汇就太美妙了。

使用Page对象能很好地做到这一点。一个我们所说的Page对象代表了一个逻辑上的web页面,它包含了最终用户在该页面上可以执行的操作。举例来说,在我们例子里面的主页就包含了三个操作:“输入用户名”、“输入密码”和“点击登录按钮”。“使用Page对象进行重构”是指把操作抽取到特定Page对象的过程,而iTest2提供了对这样的重构支持,你可以很容易做到这一点。

1. 抽取到HomePage对象

登录功能是发生在主页上面,我们把这事交给HomePage。用户登录是一个很常见的功能,我们用了三行语句(输入用户名、输入密码和点击登录按钮)完成这个操作。选中这三行代码,然后在“Refactoring”菜单下单击“Extract Page...”(快捷键是Ctrl+Alt+G)。

图1. “Refactor”菜单——“Extract Page”

如下图所示,这样会弹出一个窗口,让你输入Page对象的名字和功能名。这里,我们分别输入“HomePage”和“login”。

图2. “Extract Page”对话框

选中的3行代码就被替换成:

home_page = expect_page HomePage
home_page.login

这将会自动创建一个新文件“pages\home_page.rb”,其内容如下:

  class HomePage < RWebUnit::AbstractWebPage

    def initialize(browser)
      super(browser, "") # TODO: add identity text (in quotes)
    end

    def login
      enter_text("userName", "agileway")
      enter_text("password", "agileway")
      click_button_with_image("btn_signin.gif")
    end
  end

再次运行测试用例,它应该还是可以通过。

注意:正如Martin Fowler指出,重构的节奏:测试、小的改动、测试、小的改动。正是这种节奏保证了重构的迅速和安全。

2. 抽取SelectFlightPage

登录成功之后,顾客进入了航班选择页面。与登录页面不同,这里的每个操作很可能被不同的开发人员修改,所以我们把每个操作都抽取为一个函数。把光标移到这一行

click_radio_option("tripType", "oneway")

再次执行“Extract to Page...”重构命令(Ctrl+Alt+G),给新的Page对象和函数名输入“SelectFlightPage”和“select_trip_oneway”。

select_flight_page = expect_page SelectFlightPage
select_flight_page.select_trip_oneway

3. 继续抽取更多的操作到SelectFlightPage对象

继续把“SelectFlightPage”上的操作重构成函数:“select_from_new_york”、“select_to_sydney”和“click_continue”。

test "[1] one way trip" do
  home_page = expect_page HomePage
  home_page.login
  select_flight_page = expect_page SelectFlightPage
  select_flight_page.select_trip_oneway
  select_flight_page.select_from_new_york
  select_flight_page.select_to_sydney
  select_flight_page.click_continue
  assert_text_present("New York to Sydney")
end

跟往常一样,我们再一次运行测试用例。

编写测试用例002

在重构完测试用例001之后,我们现在有了2个Page对象(“HomePage”和“SelectFlightPage”),因此(通过重用它们)编写测试用例002会容易很多

1. 使用已有的HomePage

iTest2 IDE内置支持Page对象,输入“ep”再敲“Tab”制表键(称为“snippets”),就能自动补全为“expect_page”并且弹出所有已知的Page对象以供选择。

图 3. 自动补全Page对象

我们就能得到

expect_page HomePage

为了使用HomePage,我们需要持有它的句柄(在编程世界中,也被称为‘变量’)。执行“Introduce Page Variable”重构动作(Ctrl+Alt+V)创建一个新变量。

图 4. ‘Refactor’菜单 - “Introduce Page Variable”菜单项

home_page = expect_page HomePage

现在在新行中输入“home_page.”,会自动提示这个Page对象中定义的函数供你选择。

图 5. Page对象函数查找

2. 添加测试用例2需要的方法

测试用例002跟测试用例001很像,区别只在于旅行类型的选择和断言。借助于Recorder,我们可以定义出新的函数:

click_radio_option("tripType", "roundtrip")

把它重构成SelectFlightPage的一个新功能

select_flight_page.select_trip_round

就变成了

test "[2] round trip" do
  home_page = expect_page HomePage
  home_page.login
  select_flight_page = expect_page SelectFlightPage
  select_flight_page.select_trip_round
  select_flight_page.select_from_new_york
  select_flight_page.select_to_sydney
  select_flight_page.click_continue
  assert_text_present("New York to Sydney")
  assert_text_present("Sydney to New York")
end

运行测试用例2的测试脚本(在测试用例2的任意一行之上单击右键,选择“Run ...”),测试也通过了!

把应用复原为原始状态

但是等一等,我们还没有完成。测试用例1通过了,测试用例2也通过了,但是当把它们一起运行的时候,测试用例2却失败了,为什么?

我们没有把web应用复原回初始状态,在运行完测试用例001之后用户还是保持登录的状态。为了让测试之间互相保持独立,我们要确保每次运行测试都要以登录开始,以退出结束,有始有终。

test "[001] one way trip" do
   home_page = expect_page HomePage
   home_page.login
   # . . .
   click_link("SIGN-OFF")
   goto_page("/")
end


test "[002] round trip" do
  home_page = expect_page HomePage
  home_page.login
  # . . .
  click_link("SIGN-OFF")
  goto_page("/")
end

删除重复代码

测试脚本存在着明显的重复。RSpec框架允许用户在每个测试用例运行之前或之后执行某些操作。

选中首部两行(登录功能),按下“Shift + F7”以执行“Move Code”重构。

图 6. 重构菜单“Move code”

选择“2 Move to before(:each)”,把这部分操作移到

before(:each) do
  home_page = expect_page HomePage
  home_page.login
end

正如名字所示,这两步操作会在每个测试用例运行之前执行,所以测试用例002里面的前面两行也就没有存在的必要了。我们还可以执行相似的重构,完成“after(:each)”的相关部分。

after(:each) do

click_link("SIGN-OFF")

goto_page("/")

end

最终版本

以下是测试用例001和002的完整的(经过充分重构的)测试脚本。

load File.dirname(__FILE__) + '/test_helper.rb'


  test_suite "Complete Test Script" do
    include TestHelper


    before(:all) do
      open_browser "http://newtours.demoaut.com"
    end


    before(:each) do
      home_page = expect_page HomePage
      home_page.login
    end


    after(:each) do
      click_link("SIGN-OFF")
      goto_page("/")
    end


    test "[001] one way trip" do
      select_flight_page = expect_page SelectFlightPage
      select_flight_page.select_trip_oneway
      select_flight_page.select_from_new_york
      select_flight_page.select_to_sydney
      select_flight_page.click_continue
      assert_text_present("New York to Sydney")
    end


    test "[002] round trip" do
      select_flight_page = expect_page SelectFlightPage
      select_flight_page.select_trip_round
      select_flight_page.select_from_new_york
      select_flight_page.select_to_sydney
      select_flight_page.click_continue
      assert_text_present("New York to Sydney")
      assert_text_present("Sydney to New York")
    end


  end

适应变化

我们的世界并不完美。在软件开发行业,事物频繁发生变更。幸运的是,以上的工作使得测试脚本不仅仅更易读,而且也更容易适应变化。

1. 客户修改了术语

众所周知,项目使用同一套语言是一个好的实践,即使在测试脚本里面也是如此。举例来说,客户现在更倾向于使用“Return Trip”这个名词,而不再是“Round Trip”。借助于重构测试脚本,这很容易做到。

把光标移到“SelectFlightPage”类(pages\select_flight_page.rb)的“select_trip_round”函数,在“Refactoring”菜单下选择“Rename ...”项(Shift+F6)

图 7. “Refactor”菜单-“Rename”

然后输入新的函数名字“select_return_trip”。

图 8. “Rename Function”对话框

测试脚本其他引用“select_trip_round”的地方就都更改为

select_flight_page.select_return_trip

2. 应用程序的修改

应用程序(来自程序员)的修改就更普遍了。举例来说,程序员基于某些原因修改了航班选择页面,导致HTML页面上出发城市的属性从

<select name="fromPort">

改成

<select name="departurePort">

虽然用户不会察觉到任何变化,测试脚本(任何访问这个页面的测试用例)现在却会失败。如果你直接用录制的脚本文件作为测试脚本,修改的操作将会非常乏味,而且易于引入错误。

定位到“SelectFlightPage”的“select_from_new_york”方法(使用快捷键Ctrl+T选中“select_flight_page”,再输入快捷键Ctrl+F12选择“select_from_xx”),把“fromPort”改成“departurePort”。

def select_from_new_york
select_option("departurePort", "New York") # from 'fromPort'
end

看上去还不赖!

结论

本文我们介绍了在自动化功能测试中使用Page对象,以使测试脚本易于理解和维护。通过一个使用iTest2 IDE改善测试脚本过程的实际例子,我们演示了其提供的丰富的重构功能。

引用文献

Fowler, Martin, et al. Refactoring: Improving the design of existing code, Reading, Mass.: Addison-Wesley, 1999


感谢郑柯对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

看上去真的不赖! 发表人 王 辰阳 发表于
何止不赖,简直吸引人 发表人 maoquan xu 发表于
Re: 何止不赖,简直吸引人 发表人 Chou Jedi 发表于
Re: 何止不赖,简直吸引人 发表人 Zhimin Zhan 发表于
老大收费版本咧 发表人 Chou Jedi 发表于
比Selinium好在哪? 发表人 ha lla 发表于
感觉很不错,喜欢 发表人 zhang zhe 发表于
  1. 返回顶部

    看上去真的不赖!

    发表人 王 辰阳

    看上去真的不赖!

  2. 返回顶部

    何止不赖,简直吸引人

    发表人 maoquan xu

    不过不知道IE有没录制软件~~~~

  3. 返回顶部

    Re: 何止不赖,简直吸引人

    发表人 Chou Jedi

    我觉得作者也想开发,但给IE做这类插件是不可能的。

  4. 返回顶部

    Re: 何止不赖,简直吸引人

    发表人 Zhimin Zhan

    在iTest2 Tools 菜单里 有一个 ‘Recorder’, 可以启动IE来录制 (用OLE event),但这个功能不稳定,建议还是使用Firefox的插件。

  5. 返回顶部

    老大收费版本咧

    发表人 Chou Jedi

    收费版本咧,看着头大咧。
    LIB居然还用混淆器,还是观望下。

  6. 返回顶部

    比Selinium好在哪?

    发表人 ha lla

    比Selinium好在哪?

  7. 返回顶部

    感觉很不错,喜欢

    发表人 zhang zhe

    感觉很不错,喜欢

深度内容

应用云平台的可用性——从新浪SAE看云平台设计

云计算平台的可用性,相比传统互联网服务而言,更加复杂和困难,也更具有挑战性。本文借助新浪SAE云平台为读者讲述了云平台可用性的定义、如何打造高可用的平台,以及对云计算的用户提出了建议。

JVM定制改进 @ 淘宝

淘宝高度重视Java平台的健康发展,组建了一个团队专注于Java平台的底层部分的性能、功能与稳定性改进;工作主要基于OpenJDK中的HotSpot VM开展,其中一些通用的功能随后也会逐渐反馈给OpenJDK社区。希望能与使用Java平台开发应用的大家交流经验。
本次演讲视频录制于QCon杭州2011

"伤得起"的云计算应用——对云端应用之架构的思考

2011年4月21日至22日是值得云计算从业者纪念的日子。Amazon的IaaS服务出现故障,导致许多商业网站的服务中断,影响非常严重。作为云计算用户,我们需要思考的是,如何保证即便在云服务不可用的情况,我们的应用架构仍然能够屹立不倒?本文正是站在云计算用户的角度试图探讨这一问题。

让交付的速度跟上思考的速度

12人的技术团队,4组刀片服务器,每月20亿的访问量,每日1次准时部署,99.9%的可用性。这可能吗?当然。想知道如何做的吗?百姓网将与您分享他们在DevOps实践过程中的经验和技巧。
本次演讲视频录制于QCon杭州2011

架构之路——穿行在产品和业务之间

篱笆作为一家起源于社区的电子商务公司,反映到技术层面就是同时要面对产品和业务,以及经营战略的变化调整。如何在产品和业务的夹缝之间完成技术架构的抽象与平衡,寻找更有效的价值定位,这当中有些经验教训和个人感悟愿与众人分享。
本次演讲视频录制于QCon杭州2011

特性注入:成功三部曲

本文将对特性注入以及相关方法做一个扫盲性的介绍。我们会解释这个框架的关键要素,并附上实例来证实它们。为了让文章保持相对较短,我们不会深入到某个工具或方法中,而是会给出一些参考资料,以便大家做进一步的研究。

解析JDK 7的动态类型语言支持

随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言(Dynamically Typed Language)”支持而进行的改进之一,也是为JDK 8可以顺利实现Lambda表达式做技术准备。在这篇文章中,我们将去了解JDK 7这项新特性的出现前因后果和它的意义。

Java Remoting远程服务(下)

随着互联网应用的发展,Java分布式远程服务技术受到越来越多的关注,本文将对各种相关实现以示例的形式逐一介绍,并总结其中的优缺点,使读者能够在技术选型时有所准备。这是文章的下篇。