关键字驱动测试模式初探(old)
曾经在“我看测试”这篇文章中论述过,“测试效率的提高关键是测试手段的改进”。尤其在软件测试领域,没有千遍一律的测试方法,别人都说好的商业工具拿到你产品线来却未必合适。没有最好只有更好,如何才能产出符合淘宝框架的特色测试工具呢?之前在入淘宝之初,对淘宝架构、测试工具不甚熟悉的情况下,提出过《基于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);
}
/*****************************************代码分割线*****************************************/
这样我们第一个问题就解决了。下面来看第二个问题。
机器人可以正确执行遥控器发出的各种指令。从技术的角度说就是要求测试框架搭建人员,正确、稳定地实现遥控器中的各种指令。至于如何实现,这跟具体的产品线功能有关,这里仅给出我实现的部分代码,仅供参考:
/*****************************************代码分割线*****************************************/
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接口测试,也就是说我们的测试工程可以不再需要根据应用划分为很多个,只要这一套就可以通吃所有应用。
这一整套方案还在不断的研究实践过程中。