关键字驱动测试模式初探(old)

From

曾经在“我看测试”这篇文章中论述过,“测试效率的提高关键是测试手段的改进”。尤其在软件测试领域,没有千遍一律的测试方法,别人都说好的商业工具拿到你产品线来却未必合适。没有最好只有更好,如何才能产出符合淘宝框架的特色测试工具呢?之前在入淘宝之初,对淘宝架构、测试工具不甚熟悉的情况下,提出过《基于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(); “

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

二、测试建模

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

1) 准备米和水

2) 发出做饭指令

3) 检查饭做好了没有

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

1) 准备相关数据

2) 发出订购指令

3) 校验订购结果

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

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

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

1) 相关数据准备方便(用户关心)

2) 要有一个好的遥控器(用户不关心,制造商的事情)

3) 要有一个能正确完成指令的机器人(用户不关心,制造商的事情)

这里存在对应关系:

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

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

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

1、 制造商困扰之一——遥控器问题

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

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

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);

}

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

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

2、 制造商困扰之二——机器人问题

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

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

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;

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(\"J_PayMoney\").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测试技术。经过实践证明:

1) 对WebDriver的使用不仅方便,而且执行快速,平均一个用例5S就能运行完成

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

3) 指令正确实现以后,编写用例相当快捷方便,大大提高用例编写效率

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

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

3、 用户的困扰——数据准备问题

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

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

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

1、 在①中新建一条用例并输入用例名称,此时第②部分应该为空

2、 选择方法类型,有“@BeforeClass”,”@Before”,”@Test”,”@After”,”@AfterClass”五种选择,并从③中选择需要的“指令”

3、

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

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

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

三、测试架构

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

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

这一整套方案还在不断的研究实践过程中。

posted @ 2012-12-30 23:38  阿King2088  阅读(238)  评论(0编辑  收藏  举报