资讯点评 自动测试之关键字驱动测试模式初探

恒温 · 2013年01月04日 · 811 次阅读

Link http://www.ltesting.net/ceshi/ceshijishu/zdcs/2012/1227/205852.html

  曾经在 “我看测试” 这篇文章中论述过,“测试效率的提高关键是测试手段的改进”。尤其在软件测试领域,没有千遍一律的测试方法,别人都说好的商业工具拿到你产品线来却未必合适。没有最好只有更好,如何才能产出符合淘宝框架的特色测试工具呢?之前在入淘宝之初,对淘宝架构、测试工具不甚熟悉的情况下,提出过《基于 TTCN-3 的 Web 应用自动化测试框架》一文,但却与淘宝现有的测试工具不相符合。随着对淘宝环境逐渐熟悉,一直都在思考改进测试的方法,这种方法一定要以现在使用的 ITEST 为基础,在经过不断地实践摸索以后,结合自己的经验,提出以下测试理论,望大家参详。

  一、概念提出

  在阐述我的观点之前,先来看看下面的例子。

  在 ITEST 中,订购一个套餐的用例代码如下所示:

  /**************************************代码分割线**************************************/

  public class PlanSubTest extends BaseCase{

  final static String NICK= "leizang_test";

  final static String PASS_WORD= "taobao1234";

  @BeforeClass

  public static void login(){

  command.login(LOGIN_URL, NICK, PASS_WORD);

  }

  @AfterClass

  public static void loginOut(){

  command.loginOut();

  }

  @Before

  public void cleanDB(){

  String nick= NICK;

  command.dbExecute(

  "DELETE FROM upp_sub_plan WHERE nick= '"+ nick+ "'",

  "DELETE FROM upp_biz_order WHERE nick = '"+ nick+ "'",

  "DELETE FROM upp_prod_subscription WHERE nick = '"+ nick+ "'");

  }

  @Test

  public void test_planSub_雷藏_case01(){

  logTestName();

  //构造入参

  SubOption subOption= new SubOption(getPlanSubUrl(827L), CycleEnum.HALF_YEAR, false);

  //从页面订购

  command.doSub(subOption);

  //结果校验

  Command.checkSubResult(subOption,

  TableEnum.UPP_BIZ_ORDER,

  TableEnum.UPP_SUB_PLAN,

  TableEnum.UPP_PROD_SUBSCRIPTION);

  }

  }

  /**************************************代码分割线**************************************/

  好了,虽然例子比较简单,但足以说明问题。

  “command” 是在 “BaseCase” 中生成的一个静态的 “遥控器”(姑且这么理解):

  “protected static ActionCommand command= new ActionCommandImpl(); “

  它就像我们的电视遥控器,空调遥控器一样,一旦你拥有了它,你就可以发出遥控器所支持的各种指令。所以,下面就理所当然地发出了各种 “登录”,“退出”,“清除数据库 “,“订购”,“校验” 等各种指令,而代码就会依照我们发出的指令去执行,这就是所谓 “关键字驱动测试” 理念。

  二、测试建模

  试想一下,现在呈现在你面前的是一个万能机器人,而操控这个机器人的 “遥控器 “就在你手中,你按下” 做饭 “键,它会去做饭,你按下” 洗衣 “键,它会遵照你的命令去洗衣服。但是” 巧妇难为无米之炊 “,更何况是个没有生命的机器人。你在发出” 做饭 “指令之前,需要事先给它准备好” 米 “和” 水 “,这样它才会按照你预期的要求去做。当它完成任务的时候你需要去检查看看它完成的如何,饭做熟了没有。按照这种思路,我们对” 指挥机器人做饭 “的任务进行分解:

  <!--[if ! supportLists]-->1) <!--[endif]-->准备米和水

  <!--[if ! supportLists]-->2) <!--[endif]-->发出做饭指令

  <!--[if ! supportLists]-->3) <!--[endif]-->检查饭做好了没有

  当你把这些跟上面的测试代码联系起来思考的时候,你会发现这一切是惊人的相似。在你对套餐订购进行测试的时候,你需要做如下几件事情:

  <!--[if ! supportLists]-->1) <!--[endif]-->准备相关数据

  <!--[if ! supportLists]-->2) <!--[endif]-->发出订购指令

  <!--[if ! supportLists]-->3) <!--[endif]-->校验订购结果

  我们在编写测试用例的时候,如果能够方便地准备 “入参 “、” 预期 “,然后发出指令,代码就能自动地完成测试工作那该多好啊!

  那如何才能实现我们这一套方便、智能系统呢?

  聪明的你可能已经发现,要想达成愿望,关键在于解决以下三个难点:

  <!--[if ! supportLists]-->1) <!--[endif]-->相关数据准备方便 (用户关心)

  <!--[if ! supportLists]-->2) <!--[endif]-->要有一个好的遥控器 (用户不关心,制造商的事情)

  <!--[if ! supportLists]-->3) <!--[endif]-->要有一个能正确完成指令的机器人 (用户不关心,制造商的事情)

  这里存在对应关系:

  用户 ——>自动化用例编写者

  制造商——>测试框架搭建人员

  我们先来解决制造商的两个困扰。

  <!--[if ! supportLists]-->1、 <!--[endif]-->制造商困扰之一——遥控器问题

  遥控器就是一个各种指令的集合。在这里涉及一个问题,“如何划分指令的粒度?”#p# 分页标题 #e#

  比如说 “登录”,可以划分为:

  A.“获取登录页面”、“输入用户名”、“输入密码”、“提交” 四个指令

  也可以不进行划分

  B.就一个 “登录” 指令,包含 A 中所有步骤,只是将 “登录 URL”,“用户名”,“密码” 作为参数暴露

  这里我倾向于 B 的分法,也就是说 “将一个流程作为一个指令,将流程中所涉及的所有可变因素作为指令的参数暴露”。这样,我们只要对每个流程做好封装,以后就可以一劳永逸地重复使用它。

  从技术的角度来看,我们可以定义一个接口,并将可供用户使用的指令放置其中。代码如下:

  /**************************************代码分割线**************************************/

  /**

  * 遥控器

  * @author leizang.cs

  *

  */

  public interface ActionCommand {

  /**

  * 用户登录

  * @param url 登录 url

  * @param nick 用户名

  * @param passWord 密码

  */

  public void login(String url, String nick, String passWord);

  /**

  * 退出

  */

  public void loginOut();

  /**

  * 执行订购

  * @param subOption 订购入参

  */

  public void doSub(SubOption subOption);

  /**

  * 订购成功后校验数据库

  * @param dbCheckOption 校验入参

  * @param needCheckedTables 需要校验的表格

  */

  public void checkSubDB(SubDbCheckOption dbCheckOption, TableEnum...needCheckedTables);

  /**

  * 数据库修改或删除

  * @param sql 需要执行的 sql

  */

  public void dbExecute(String... sqls);

  }

  /**************************************代码分割线**************************************/

  这样我们第一个问题就解决了。下面来看第二个问题。

  <!--[if ! supportLists]-->2、 <!--[endif]-->制造商困扰之二——机器人问题

  机器人可以正确执行遥控器发出的各种指令。从技术的角度说就是要求测试框架搭建人员,正确、稳定地实现遥控器中的各种指令。至于如何实现,这跟具体的产品线功能有关,这里仅给出我实现的部分代码,仅供参考:

  /**************************************代码分割线**************************************/

  public class ActionCommandImpl implements ActionCommand{

  private WebDriver driver;

  private JdbcTemplate jdbc;

  @Override

  public void dbExecute(String... sqls){

  for(String sql: sqls){

  jdbc= CommonUtil.getJdbcFromSql(sql);

  jdbc.execute(sql);

  }

  }

  @Override

  public void login(String url, String nick, String passWord){

  try{

  driver= new HtmlUnitDriver();

  driver.get(url);

  WebElement userName= driver.findElement(By.id("TPL_username_1"));

  userName.sendKeys(nick);

  WebElement passWd= driver.findElement(By.name("TPL_password"));

  passWd.sendKeys(passWord);

  WebElement submit= driver.findElement(By.className("J_Submit"));

  submit.click();

  }finally{

  writePage();

  }

  }

  @Override

  public void loginOut(){

  driver.quit();

  }

  /**

  * @dscription 订购接口

  * @param subOption 订购参数

  * @throws ITestException

  */

  @Override

  public void doSub(SubOption subOption) throws ITestException{

  if(subOption== null){

  Assert.fail("订购参数不能为空!");

  }

  String subUrl= subOption.getSubUrl();

  CycleEnum cycle= subOption.getCycle();

  log("传入参数为:");

  look(subOption);

  if(subUrl== null || subUrl.isEmpty()){

  Assert.fail("订购 Url 不能为空!");

  }

  if(cycle== null){

  Assert.fail("订购周期不能为空!");

  }

  try{

  driver.get(subUrl);

  log("\n 获取页面:"+ subUrl);

  WebElement period= null;

  switch(cycle){

  case ONE_MONTH:

  period=driver.findElement(By.id("p-month"));

  period.setSelected();

  break;

  case ONE_SEASON:

  period=driver.findElement(By.id("p-season"));

  period.setSelected();

  break;

  case HALF_YEAR:

  period=driver.findElement(By.id("p-half"));

  period.setSelected();

  break;#p# 分页标题 #e#

  case ONE_YEAR:

  period=driver.findElement(By.id("p-half"));

  period.setSelected();

  break;

  default:

  Assert.fail("入参中周期值不合法!");

  }

  WebElement isAgree= driver.findElement(By.id("J_Agreement"));

  isAgree.click();

  ((HtmlUnitDriver) driver).setJavascriptEnabled(true);

  String js= "document.getElementById(&quot;J_PayMoney&quot;).disabled = false";

  ((HtmlUnitDriver) driver).executeScript(js);

  log("执行 JS:"+ js);

  WebElement payMoney= driver.findElement(By.id("J_PayMoney"));

  String prePayUrl= driver.getCurrentUrl();

  payMoney.click();

  String afterPayUrl= driver.getCurrentUrl();

  if(! isPageSkip(prePayUrl, afterPayUrl)){

  throw new ITestException("订购失败! 请查看"+ DIRECT+ "目录确认页面信息\n");

  }

  WebElement bd= driver.findElement(By.className("bd"));

  log("\n"+ bd.getText());

  }catch(NoSuchElementException e1){

  throw new ITestException(e1);

  }finally{

  writePage();

  }

  }

  /**

  *

  * @param dbCheckOption 数据库校验参数

  * @param checkedTables 需要校验的表

  */

  @Override

  public void checkSubDB(SubDbCheckOption dbCheckOption, TableEnum...needCheckedTables){

  for(TableEnum table: needCheckedTables){

  log("\n");

  switch(table){

  case UPP_BIZ_ORDER:

  checkUppBizOrder(dbCheckOption);

  break;

  case UPP_SUB_PLAN:

  checkUppPlanSub(dbCheckOption);

  break;

  case UPP_PROD_SUBSCRIPTION:

  checkUppProdSubscription(dbCheckOption);

  break;

  default:

  Assert.fail("暂无此表校验逻辑:"+ table.name());

  }

  }

  }

  /**************************************代码分割线**************************************/

  在这里我引入了 JAVA 的 GUI 测试技术。经过实践证明:

  <!--[if ! supportLists]-->1) <!--[endif]-->对 WebDriver 的使用不仅方便,而且执行快速,平均一个用例 5S 就能运行完成

  <!--[if ! supportLists]-->2) <!--[endif]-->更重要的是测试代码完全独立于开发代码,测试环境最接近真实的手工测试环境,用这种方法实现的自动化,只是模拟手工测试工作,并将其自动进行

  <!--[if ! supportLists]-->3) <!--[endif]-->指令正确实现以后,编写用例相当快捷方便,大大提高用例编写效率

  <!--[if ! supportLists]-->4) <!--[endif]-->脚本稳定、健壮且易于维护,只要页面不发生变化,对指令的实现就无需变化,大大降低维护成本

  这样,上面提出的两个问题就解决了,我们编写出的代码就会像第一节所示的一样,只要准备好相关数据,发发指令就可以了。下面我们来解决用户的困扰。

  <!--[if ! supportLists]-->3、 <!--[endif]-->用户的困扰——数据准备问题

  还记得上节划分指令粒度的时候我们是按流程来划分的吗?在这里它的好处就体现出来了。我们把一个流程作为一个指令,将流程中涉及的可变因素作为参数暴露,并将指令在接口中定义,实现与定义分开,这样对于每一个指令来说,其参数个数是固定的,而且对于每条产品线来说指令的个数也比较有限。这非常有利于我们将其 “模板化”。说到模板化,大家自然会想到界面,于是就有三种方式进行模板化:“页面”,“软件客户端”,“eclipse 插件”。我认为最简单、最方便的当属 “eclipse 插件。” 下面我给出插件示意图:

  <!--[endif]-->

  最左边①是用例的目录树,当选中一条用例后第②部分为该用例的有序指令,第③部分为 “指令池”,可以从中选择需要的 “指令”。

  这样我们编写用例就可以分为三步:

  <!--[if ! supportLists]-->1、 <!--[endif]-->在①中新建一条用例并输入用例名称,此时第②部分应该为空

  <!--[if ! supportLists]-->2、 <!--[endif]-->选择方法类型,有 “@BeforeClass”,”@Before”,”@Test”,”@After”,”@AfterClass” 五种选择,并从③中选择需要的 “指令”

  <!--[if ! supportLists]-->3、 <!--[endif]-->

  <!--[if ! supportLists]-->3、 <!--[endif]-->存据填入期击 ommand 录入数据,双击 “doSub” 指令,此时弹出如下图所示的参数录入框,将数据填入其内并保存,保存后在 eclipse 中就会自动生成如第一节所列出的用例代码,其中 “SUB_PLAN_URL” 为 poperties 中定义的变量,也可以在界面中进行关联、维护。

#p# 分页标题 #e#

  由此可见,用户只和界面打交道,在此进行用例的增、删、改、执行操作。这样,用例设计人员专心设计场景与用例,测试框架维护人员维护自己产品线的框架,分工协作,效率大大提升。

  好了,到此为止,我们所有的困难都解决了,下面给出该套测试框架的架构图。

  三、测试架构

  根据上面的论述,不难得出如下图所示的测试架构图:

  “话说天下大势,分久必合,合久必分”,其不仅可用于 WEB 层测试,也可以用作 HSF 接口测试,也就是说我们的测试工程可以不再需要根据应用划分为很多个,只要这一套就可以通吃所有应用。

  这一整套方案还在不断的研究实践过程中。
文章分类:自动测试

Link http://www.ltesting.net/ceshi/ceshijishu/zdcs/2012/1227/205852.html

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册