当今,“自动化测试”大行其道,其中“接口自动化测试”便是同行们谈得最多的话题之一。了解测试金字塔分层理念的童鞋都清楚,接口自动化测试有以下优点。
- 投入低,产出高。
- 比UI自动化更稳定。
- 比单元测试更接近真实业务。
正因为以上优点,接口自动化测试逐渐成为了业界主流,各种工具/框架层出不穷,比如Postman,Jmeter,Htttpclient,Rest assured,HttpRunnerManager等。
二、背景 此前笔者曾基于Jenkins+Ant+Git+Jmeter搭建过一套接口自动化框架,期间亦针对Jmeter做了许多功能的扩展,比如:生成excle结果文件、数据库断言、自动提交缺陷、自动更新案例执行结果至Testlink等。虽说Jmeter简单易上手,但在笔者看来,其并不是接口自动化测试的首选,其中的原因暂不祥谈,毕竟仁者见仁。 近段时间,笔者一直在思索,学习前辈们优秀的经验,并从公司项目架构出发,搭建了一套基于Jenkins+Maven+Git+TestNG+RestAssured+Allure的持续集成测试框架,相比原先Jmeter的那套,其易用性更好、效率更高、扩展性更强。
三、框架理念- 接口自动化测试框架
- 根据用例模板编写测试用例(包含响应报文断言及接口入参等)。
- 编写数据库断言文件。
- 编写测试类。
- 配置环境及测试用例集。
- Jenkins选择运行环境及用例集,触发构建。
- 生成测试报告,邮件通知相关人员。
用例文件名称与测试类名一致,比如开户的测试类名为OpenAcc,则用例文件名为OpenAcc.xls,用例模板由以下几部分组成。
- caseNo、testPoint分别与案例管理系统的案例编号、案例标题对应,方便后续同步执行结果。
- preResult为响应报文断言,目前支持jsonpath、包含断言、不包含断言。
- YorN为案例执行标志,Y表示执行。
- tableCheck为数据库断言标志,Y表示进行数据库断言。
- 其他字段为接口入参,目前支持以下五种供数方式。
(1)自定义函数供数。引用格式为:__phone()表示生成11位手机号。__idno()表示生成18位身份证号。
(2)查询数据池供数。引用格式为:${dp.sql(select accountNo from M_account where status = 1)}
(3)查询数据库供数。引用格式为:${db.sql(select accountNo from M_account_card where status = 1)}
(4)先接口请求,然后提取响应报文供数。引用格式为:KaTeX parse error: Expected '}', got 'EOF' at end of input: …g.case023.post(.data.code)},表示先以post方式发送SendmsgYg接口请求,然后再提取响应报文的code字段。支持接口之间的多重依赖。
(5)先接口请求,然后查询数据库/池供数。引用格式为:${SendmsgYg.case023.post.db.sql(select accountNo from M_account_card where status = 1)},表示先以post方式发送SendmsgYg接口请求,然后再查询数据库(db)/数据池(dp)获取数据。
2、编写数据库断言数据库断言文件名称与测试类名一致,比如开户的测试类名为OpenAcc,则断言文件为OpenAcc.xml。一个接口对应一个数据库断言文件,一个断言文件里可包含多条案例,每条案例可以断言多张表,模板如下。 ps:为了提升效率,后续亦会提供一个生成数据库断言文件小工具。
ACCOUNT_NO
CUST_ID
MERCHANT_ID
1
0019901
ACCOUNT_NO
CARD_NO
2
MERCHANT_ID
CUST_ID
CARD_IMG
NOTNULL
对于未确定的预期结果,使用变量代替,后续编写测试类时再做映射。
3、编写测试类测试类分为两大类,一是只需响应报文断言,二是需要响应报文及数据库断言。测试人员按照以下模板编写脚本即可。
/*
*短信发送接口
* 环境参数在SetUpTearDown 父类定义
*/
@Feature("分类账户改造")
public class SendmsgYg extends SetUpTearDown {
@Story("发送短信")
@Test(dataProvider = "dataprovider",
dataProviderClass = DataProviders.class,
description = "发送短信")
public void runCase(String caseMess, String bodyString) throws IOException, SQLException, ClassNotFoundException {
//发送请求
Response response = RunCaseJson.runCase(bodyString, "post");
//只进行响应报文断言
asserts(caseMess, bodyString, response.asString(),"",null);
}
}
数据库断言文件中的变量,可通过调用封装的方法取值,比如查数据库、提取响应报文、调用接口等方式。
/*
*开立分类账户
* 环境参数在SetUpTearDown 父类定义
*/
@Feature("分类账户改造")
public class OpenYg extends SetUpTearDown {
@Story("分类账户开户")
@Test(dataProvider = "dataprovider",
dataProviderClass = DataProviders.class,
description = "开户")
public void runCase(String caseMess, String bodyString) throws IOException, SQLException, ClassNotFoundException {
//发送请求
Response response = RunCaseJson.runCase(bodyString, "post");
//如果需要数据库断言,此处添加断言文件变量的map映射
//可通过调用封装的方法取值,比如查数据库、提取响应报文、调用接口等方式。
Map map = new HashMap();
//查询数据库获取,取不到值返回""
String account = DataBaseCRUD.selectData("select accountNo from M_ACCOUNT where status =1");
//提取响应报文,取不到值返回""
String custId = RespondAssertForJson.getBuildValue(response.asString(),"$.data.custid");
//执行SendmsgYg接口的case023案例,然后提取响应报文的merchanId ,取不到值返回""
String merchanId = RespondAssertForJson.getBuildValue("","${SendmsgYg.case023.post($.data.merchanId)}");
map.put("ACCOUNT_NO",account);
map.put("CUST_ID",custId);
map.put("MERCHANT_ID",merchanId);
//断言(包含响应报文断言和数据库断言)
String xmlFileName = this.getClass().getSimpleName(); //数据库断言xml文件名(与类名保持一致)
asserts(caseMess, bodyString, response.asString(),xmlFileName,map);
}
}
4、用例集
对于多个suite,可通过suite-files配置。testng.xml文件配置如下。
5、Jenkins构建
选择环境及测试用例集,开始构建,构建完成后生成测试报告及日志。也可根据需要设置定时构建,持续进行质量监控。
在这个注重颜值的世界,allure框架出来的测试报告绝对称得上“报告界的小鲜肉”。
测试报告总览包含用例通过率、测试套件、环境、feature、类别、趋势等信息。以下示例截图的案例全部执行失败,所以总览的通过率是0%。
类别主要展现失败的用例信息,可根据项目情况自定制报告内容,比如请求报文、响应报文、断言结果等。
时间刻度展现了每条案例的执行时间。
- Jenkins
- Maven
- Gitlab
- TestNG
- Rest Assured
- Allure
支持多环境(sit,uat)切换,结合Jenkins使用。
4.0.0
HFIIACCOUNT
ApiAutoTest
1.0-SNAPSHOT
1.8.10
UTF-8
org.testng
testng
6.11
io.rest-assured
rest-assured
3.1.0
ru.yandex.qatools.allure
allure-testng-adaptor
1.3.6
org.testng
testng
io.qameta.allure
allure-testng
2.0-BETA14
net.sourceforge.jexcelapi
jxl
2.6.12
com.google.code.gson
gson
2.8.2
com.alibaba
fastjson
1.2.13
com.oracle
ojdbc14
10.2.0.4.0
src/main/resources
true
src/main/filters/filter_${env}.properties
org.apache.maven.plugins
maven-surefire-plugin
2.20
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
allure.results.directory
./target/allure-results
org.aspectj
aspectjweaver
${aspectj.version}
org.apache.maven.plugins
maven-surefire-plugin
2.19
src/main/resources/testngXml/${xmlFileName}
${project.basedir}/target/classes/testngXml/${xmlFileName}
org.apache.maven.plugins
maven-compiler-plugin
8
8
org.apache.maven.plugins
maven-resources-plugin
2.6
xls
uat
uat
sit
sit
true
4、实现思路
- 将每个接口的案例读取到Map保存,其中key值为每条案例的caseNo+testPoint+preResult+YorN+tableCheck拼装(也是一个map),value值为剩余字段(接口入参)的拼装,即接口请求的bodyString。
public static void getMap(Sheet sheet, int cols, int row, String pubArgs){
for (int col = 0; col = 5) {
//appid,api,version属于公共入参,公共入参字段在PublicArgs.properties文件进行配置
// getBuildValue(value1,value2)方法用于转换${}或者函数为对应的值
if (pubArgs.toLowerCase().contains(cellKey.toLowerCase().trim())) {
bodyMap.put(cellKey, RespondAssertForJson.getBuildValue("", sheet.getCell(col, row).getContents()));
} else {
dataMap.put(cellKey, RespondAssertForJson.getBuildValue("", sheet.getCell(col, row).getContents()));
}
} else {
caseMessMap.put(cellKey, cellValue);
}
}
bodyMap.put("data", dataMap);
map.put(new Gson().toJson(caseMessMap), new Gson().toJson(bodyMap));
}
- 对于案例中的自定义函数或供数变量,封装了方法RespondAssertForJson.getBuildValue(var1,var2)进行转换,目前暂支持前文提到的五种供数方式,后续可根据需要进行扩展。
/**
* 支持json串转换
* 支持自定义函数的转换
* 支持${}变量转换
*
* @param sourchJson
* @param key
* @return
*/
public static String getBuildValue(String sourchJson, String key) {
key = key.trim();
Matcher funMatch = funPattern.matcher(key);
Matcher replacePattern = replaceParamPattern.matcher(key);
log.info("key is:" + key);
try{
if (key.startsWith("$.")) {// jsonpath
key = JSONPath.read(sourchJson, key).toString(); //jsonpath读取对应的值
log.info("key start with $.,value is:" + key);
} else if (funMatch.find()) {//函数
String args = funMatch.group(2); //函数入参
log.info("key is a function,args is:" + args);
String[] argArr = args.split(",");
for (int index = 0; index
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?