最近利用些业余时间自己编写了一个小型自动化测试框架,在设计过程中自己也渐渐对自动化框架的作用有了些新的认识,希望能和大家分享一下。
其实设计这个框架最初的动机是来源于工作中的一个任务——同事让我维护一个很久以前编写的“自动化脚本”,难度不大,只是一串处理和过程,看懂代码以后只需要修改个别逻辑和参数即可。但后来我想了想,这样纯粹只有过程的脚本,在开发测试时用来当做小工具用不错,但一旦需要建立稳定的自动化测试机制,有大量功能点和测试数据的时候就会显得力不从心。一个功能点对应一个脚本,新增功能点甚至测试数据都需要对应增加脚本,开发维护的成本则会非常高。
后来我自己尝试去做了一个小型的自动化测试框架,虽然花费了不少时间才实现了原来脚本的内容,但是磨刀不误砍柴工,有了框架,接下来新增功能点的开发工作量大大减轻。自己在设计该框架时也基本上是摸着石头过河,一边思考自动化框架究竟需要做什么,一边也参考一些开源自动化框架例如Ruby Watir的设计方式,以下便是我总结的一些经验和心得。
一、测试脚本与测试框架脱离
我开头提到的那个“测试脚本”,从程序启动,测试动作执行,测试结果反馈都一手包干,例如对于A1和A2两个相似的功能点,其测试代码如下:
功能A-1的脚本:A1Test.java
public static void main(String[] args) throws Exception {
// A1测试逻辑实现
……
Class.forName("oracle.jdbc.driver.OracleDriver");
String url = "jdbc:oracle:thin:@localhost:1521:cui";
Connection conn = DriverManager.getConnection(url, "cui", "cui");
……
}
功能A-2的脚本:A2Test.java
public static void main(String[] args) throws Exception {
// A2测试逻辑实现
……
Connection conn = null;
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
String connectionUrl = "jdbc:oracle:thin:@localhost:1521:cui";
conn = DriverManager.getConnection(connectionUrl, "cui", "cui");
……
}
这样一来A1和A2的测试脚本都可以独立运行,也不需要什么自动化框架,但是原本相似的A1和A2功能,却因为这样的架构要写两次代码,如果TestA1和TestA2由两个不同的程序员编写,那么即便是如上面所示连接一个相同的数据库,每个人需要自己写出实现方式,且都可能有不同的代码风格,这样极大增加了测试代码的编写和维护成本。
对于测试脚本而言,仅仅只需要负责测试逻辑本身,不应该负责诸如脚本启动,管理的功能,同时降低提升测试脚本编写和维护成本,一些公用的方法例如数据库连接等,都最好封装成方法放在测试框架中,然后供测试脚本调用。
我在编写自动化脚本时,将每个测试功能点脚本作为一个Scenario类供测试框架调用的,测试框架可以根据用户输入或者任务设定选择执行哪些脚本。
TestFramework.excute(Scenario userInputScenarioName);
同时,对于如数据库连接查询这样的操作,也都做了封装,在每个Scenario脚本里,编写者可以通过1行代码就能查询想要的数据:
在配置文件中填入数据库信息:
#别名 db_dev #类型 Oracle #IP 127.0.0.1 #端口 1521 #数据库名 db1 #用户名 cui #密码 cui
在脚本文件中就可以这样来访问数据库:
String id = DB("db_dev").getSingleResult("select id from ……");
数据库的链接,关闭工作都由框架进行统一封装。
二、测试数据与测试脚本脱离
测试脚本与测试框架脱离后,测试脚本更加专注于业务逻辑,但仅仅这样是不是就够了呢?对于同一个功能点,我们往往也需要测很多数据,如果每个测试数据都“硬编码”到测试脚本里,那么数据增加的时候又要将硬编码的部分代码复制粘贴,犯了和先前一样的毛病:
// 测试脚本脱离测试框架后的A1Test
class A1Test() {
……
// 测试A的实现过程
id = DB("db_dev").getSingleResult("select id from …… where coutry = 'CN'");
Assert(id == 100000);
id = DB("db_dev").getSingleResult("select id from …… where coutry = 'US'");
Assert(id == 200000);
id = DB("db_dev").getSingleResult("select id from …… where coutry = 'CA'");
Assert(id == 300000);
……
}
我们可以看到,虽然测试脚本TestA1的确不再负责程序启动这样的杂事,同时数据链接也更加方便和规范,但是对于多个用例(country值不同,需要校验的id大小不同),仍然需要复制粘贴代码来实现。
因此我们也需要将测试数据从测试脚本中独立出来,实现“一个框架对应多个测试脚本,一个测试脚本对应多个测试数据”。
对于这样的测试数据,往往是相对整齐规范的,我们可以用Excel,txt等文件,用表格的方式存储测试数据,然后写出程序逐行读取(jxl可以支持Excel2003以前Excel文件的读取),每一行就是单次测试所需的数据。
用例ID 用例描述 是否执行 国家缩写 期望结果ID
用例ID | 用例描述 | 是否执行 | 国家缩写 | 期望结果ID |
1 | 测试CN对应ID | y | CN | 100000 |
2 | 测试US对应ID | y | US | 200000 |
3 | 测试CA对应ID | y | CA | 300000 |
我采用jxl去读取Excel数据,同时再对参数取用的方法进行简化和封装,最终可以用例如param("期望结果")这样的方式返回当前执行的数据行对应列的数据。
// 测试数据与测试脚本脱离后的A1Test
class A1Test() {
……
// 测试A的实现过程
id = DB("db_dev").getSingleResult("select id from …… where coutry = " + param("国家缩写"));
Assert(id == param("期望结果ID"));
……
}
三、总结
测试脚本从测试框架脱离,即是将“一个测试脚本负责整个测试执行过程”的设计思路变为“测试脚本只负责业务逻辑,一个测试框架驱动多个测试脚本完成测试”。当业务逻辑变化时,我们可以只修改测试脚本和数据而无须修改测试框架,当测试数据需要增加和修改时,我们可以只修改测试数据而无须修改测试脚本,这样的思路归根结底还是来源于面向对象,但不管怎样,提高效率,降低成本才是最终的目的。
文章来源于领测软件测试网 https://www.ltesting.net/