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";
public static void login(){
command.login(LOGIN_URL, NICK, PASS_WORD);
}
public static void loginOut(){
command.loginOut();
}
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+ "'");
}
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;
public void dbExecute(String... sqls){
for(String sql: sqls){
jdbc= CommonUtil.getJdbcFromSql(sql);
jdbc.execute(sql);
}
}
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();
}
}
public void loginOut(){
driver.quit();
}
/**
* @dscription 订购接口
* @param subOption 订购参数
* @throws ITestException
*/
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("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 需要校验的表
*/
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