一、目的:
1、为什么我们需要做接口自动化?
1、当业务接口需要迁移或重构时,直接执行自动化脚本确保主流程正常可用,特别是业务线多且复杂的项目,节省大量测试时间。
2、后端开发已经完成接口开发需要调通接口时,只需要跑通所给的接口自动化脚本完成自测便可以给前端开发调试,节省开发自测及联调时间
3、git+jekins+jmeter+csv+ant线上持续集成,监控线上主业务的接口情况
2、为何选择jmeter?
1、jmeter功能非常强大,自己封装了很多已有的代码逻辑在里面,例如if控制器、循环控制器、bean shell处理程序等
2、熟练使用jmeter对于后面的接口性能测试有一定的帮助(有点牵强)
二、jmeter安装
1、百度查询安装及环境配置,目前我使用的版本是5.1.1
2、这篇分享默认大家已经有一点jmeter基础
三、token准备
1、新建测试计划
2、新建token线程组
3、执行线程查看返回token,提取token后设置全局变量
这样我们已经得到了一个获取token的全局变量,直接调用${__P(tokenG,)}即可获取
我目前的需求只需要这一个token即可,如果需要多个token该怎么处理,继续往下看,你们会有思路。
4、前面已经获取到了全局的token,只需要在每个接口的请求头设置就可以了
目前我们的前期token已经准备完成,需要来写业务接口逻辑了
业务接口脚本
1、在上面我们已经解决了token的问题,现在我们开始着手解决业务接口的问题
2、现在我们需要用到csv把所有接口的参数全部放在csv中统一管理,这样维护成本较低,不用修改原有的脚本
在这里我们就需要使用循环控制器+CSV了,首先看看CSV需要填写的字段
新建循环控制器
在循环控制器内添加CSV文件设置
目前我们的csv已经配置完成,现在我们开始要写HTTP请求了
在循环控制器里面,读取的CSV数据是一条数据为一次循环,因此我们需要做一次请求方式的判断,把不同请求方式的HTTP请求区分开来,这样减少了脚本的复杂性,几百个接口的请求,不用写几百个HTTP请求
记住循环控制器的次数要与CSV用例行数一致(每一次循环只能读一条数据忽略首行)
新建if控制器来区分请求方式,有几种请求方式就新建几个if控制器
先查看CSV文件的数据
在CSV数据文件设置中,已经把每一列都设置成为了变量,直接调用即可例如:
${casename} 直接拿到当前循环的用例名
在CSV写入数据开始跑写好的脚本
对比CSV数据会发现,POST只有“查看工作台”,GET只有“获取用户卡片”说明执行成功,如果接口数很多,我们只需在CSV中添加数据即可,这样写节省了很多不必要的脚本。
感觉我们已经完成了CSV的数据控制已经完成了自动化,但是接口请求的要求是不一样的,后面的路还有很多,下面四个问题需要我们去处理使脚本更加灵活
1、接口数据的继承怎么完成,前面接口的返回值怎么给后面的接口调用
2、如果只是这样处理CSV,那么我们只能跑每一个单接口的用例,对于业务线来说没有任何意义
3、断言该怎么处理,有些接口有返回值,有些接口成功了也没有返回值
4、接口执行成功了,但是返回的数据可能为空,怎么确认数据改变了
上一个接口返回值的提取
首先我们来处理第一个问题,接口返回值的传递
1、有些接口需要前面接口的返回值,但是有些接口不需要,我们就需要用到我们的CSV另一个字段needdata
继续在if控制器添加一个if控制器
既然我们需要上一个接口返回的参数,那么肯定需要上一个接口先执行,我们直接去提取它的数据,这里就需要用到jmeter自带的后置处理器–bean shell后置处理器
是的,我们要开始写代码了!!!(之前一直写python,java是临时学的,代码不是很优雅)
下面代码的目的
1、首先needdata这个字段主要是获取当前接口返回的字段
2、当我们拿到接口返回值时,可能是如下的结果
1、{"items":[{"id":1234,"name":"张三"},{"id":5678,"name":"李四"}]
那么上面这样的返回值我想拿到name等于张三的id怎么处理?
按照代码处理我们需要先get(items)然后再继续往下找,万一我们需要的值在第五层,
我们不可能去get五层去找,何况每个接口需要字段的层级不同,所以我们把返回值处理了让所有返回值都在第一个层级得到:{"id":5678,"name":"李四"}
2、其实一般情况下我们需要的只是某一个字段或者某几个字段,因为这种写法会出现后面相同key覆盖前面key的值,例如{"id":5678,"name":"李四"}覆盖了{"id":1234,"name":"张三"}
这样我们不能拿到“张三”,所以我们加入了basis进行校验,在平时接口中我们都会知道name的值,但是id只有接口返回才会知道具体值,加入basis{"name":"张三"}
那么代码会只处理到{"id":1234,"name":"张三"},不会继续往下处理李四的信息,这样我们所有的字段只用get(needdata)就可以拿到想要的值了。
如果想看python的代码,在文章最后会贴上代码
import org.json.*;
import org.json.JSONObject;//现在开始需要json.jar包
import org.json.JSONArray;
import java.util.*;
//内置函数获取变量的值,先提取csv中needdata的值
String ids = vars.get("needdata");
String basi =vars.get("basis");
log.info("我传的参数----"+ids);
String response_data = prev.getResponseDataAsString();
log.info("我传的参数----"+response_data);
public static void JsonToMap( Stack stObj, Map resultMap,JSONObject basis) throws Exception {
if(stObj == null && stObj.pop() == null){
return ;
}
if (stObj.empty()) return;
JSONObject json = stObj.pop();
Iterator it = json.keys();
while(it.hasNext()){
String key = (String) it.next();
//得到value的值
Object value = json.get(key);
//System.out.println(value);
if(value instanceof JSONObject)
{
stObj.push((JSONObject)value);
//递归遍历
JsonToMap(stObj,resultMap,basis);
} else if(value instanceof JSONArray) {
for(int i = 0; i < ((JSONArray) value).length(); i++) {
stObj.push(((JSONArray) value).getJSONObject(i));
//当遇到basis就停止继续写入键值对
if(!basis.isEmpty()){
JSONObject tmp = ((JSONArray) value).getJSONObject(i);
String paramsKey = basis.keys().next();
String paramsValue = basis.get(paramsKey).toString();
String tmps;
//捕获异常,返回值没有对应的Key直接跳过
try{
tmps = tmp.get(paramsKey).toString();
}catch(JSONException ex){
continue;
}
if (tmps.equals(paramsValue)){
break;
}
}
}
JsonToMap(stObj,resultMap,basis);
} else {
resultMap.put(key, value);
}
}
}
public static void JsonToMap( Stack stObj, Map resultMap) throws Exception {
if(stObj == null && stObj.pop() == null){
return ;
}
if (stObj.empty()) return;
JSONObject json = stObj.pop();
Iterator it = json.keys();
while(it.hasNext()){
String key = (String) it.next();
//得到value的值
Object value = json.get(key);
//System.out.println(value);
if(value instanceof JSONObject)
{
stObj.push((JSONObject)value);
//递归遍历
JsonToMap(stObj,resultMap);
} else if(value instanceof JSONArray) {
for(int i = 0; i < ((JSONArray) value).length(); i++) {
stObj.push(((JSONArray) value).getJSONObject(i));
}
JsonToMap(stObj,resultMap);
} else {
resultMap.put(key, value);
}
}
}
public static void lalam(String json,String param,String basi) throws Exception {
String pre = "{\"object\":";
String post = "}";
String jsonStr = pre + json + post;
JSONObject basis = null;
if (!basi.isEmpty()) basis = new JSONObject("{"+basi+"}");
JSONObject obj = new JSONObject(jsonStr);
Stack stObj = new Stack();
stObj.push(obj);
Map resultMap =new HashMap();
if(basis!=null){
JsonToMap(stObj,resultMap,basis);
}
else{
JsonToMap(stObj,resultMap);
}
Set keys = resultMap.keySet();
String[] strArr = ids.split(",");
for (int i=0;i< strArr.length;++i){
resultMap.get(strArr[i]);
String logs = resultMap.get(strArr[i]).toString();
log.info(logs);
//内置方法,定义变量和值
vars.put(strArr[i].toString(),logs);
}
}
lalam(response_data,ids,basi);
根据以上代码我们拿到了自己想拿到的值,我们还差一步,那就是怎么把值放进去,
jmeter强大的地方就是提供了很多内置方法可以在执行http请求前修改填写好的参数
执行前处理需要填写的字段
我们在每个请求前加入前置处理器,在执行请求前先执行脚本替换值就可以了
代码处理很简单,如果URL上有{}的字段就直接去替换对应的值,正文如果有<>的字段就直接去替换;当然你也可以用其他符号去判断他是否要去替换,get请求只用处理url就行
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONObject;
Arguments arguments = sampler.getArguments();
Argument argument = arguments;
//String path = ctx.getCurrentSampler().toString();
//post请求拿URL一定要用getUrl,因为CTX会拿到其他值
String path = sampler.getUrl().toString();
public static List getData(String data){
String skh ="\\{([^}]*)\\}";
List list= new ArrayList();
Pattern pattern=Pattern.compile(skh);
Matcher m = pattern.matcher(data);
while(m.find()){
list.add(m.group().substring(1, m.group().length()-1));
}
return list;
}
log.info("---"+path);
//如果url中出现了{符号就去执行替换
boolean status = path.contains("{");
if (status){
List list = getData(path);
for(int i=0;i<list.size();i++){
String para = list.get(i);
String paras = "{"+para+"}";
path = path.replaceAll("\\"+paras, vars.get(para));
sampler.setPath(path);
};
};
//拿到了当前请求的正文数据
String value = argument.getArgument(0).getValue();
String name = argument.getName();
public static List bodyData(String data){
String skh ="\\<.*?\\>";
List list= new ArrayList();
Pattern pattern=Pattern.compile(skh);
Matcher m = pattern.matcher(data);
while(m.find()){
list.add(m.group().substring(1, m.group().length()-1));
}
return list;
}
//如果请求正文中有<符号就去替换
boolean statues = value.contains("<");
if(statues){
log.info(value);
List list = bodyData(value);
for(int i= 0;i< list.size();i++){
String param = list.get(i);
String params = "<"+param+">";
value = value.replaceAll("\\"+params,vars.get(param));
log.info(vars.get(param).toString());
};
Arguments arg = new Arguments();
HTTPArgument newArg = new HTTPArgument();
newArg.setValue(value);
//添加了新的请求正文
arg.addArgument(newArg);
//把修改后的请求正文设置成当前请求的正文
sampler.setArguments(arg);
log.info(value);
}
到目前为止我们前置处理和后置处理的代码以及全部处理完成,现在我们来试试
我们已经处理了字段传递的情况,如果你有多个请求方式,直接复制粘贴改下if控制器的判断请求方式就可以完成了
处理断言
我们已经很好的处理了接口参数问题,但是对于是否执行成功,或者参数是否返回正确,我们不能按照工具自己默认的正确与否去判断,这里我们还有工作要做。
断言脚本:
//代码非常简单,我只是做了一层判断而已,因此我们需要csv的两个字段,assert和code,assert是字段例如“name”:"张三",
如果返回值包含了这个字段才会提示通过,有的接口返回值是空,所以我们不可能用assert去判断,因此加入了code,
对比返回值和我们给的返回值是否一致
String json = prev.getResponseDataAsString();
String asserts = vars.get("assert");
String code = vars.get("code");
//在csv中写入接口返回值应该出现的字段,然后返回值去判断该字段和值是否包含在内
if(!asserts.isEmpty()){
if(json.contains(asserts)){
Failure=false;
}
else{
Failure = true;
FailureMessage = json;
}
}
//有的接口返回值为空,这个时候只能去判断当前接口的返回值是否一致
else{
log.info("返回的状态码"+ResponseCode.toString());
if(code.equals(ResponseCode.toString())){
Failure=false;
}
else{
Failure = true;
FailureMessage = json;
}
}
我们来看看断言的执行情况:
到目前为止我们相当于完成了所有的单接口执行,但是对于我们业务来说这个肯定远远不够,我们需要的是组合用例,也就是一个主功能需要几个接口一起合作完成的,开始加入事务控制器。
留下问题:一个接口你就判断200后,只代表这个接口执行成功了,数据是否改变还不清楚
加入事务控制器
事务控制器的加入对于我们业务流程是一个很好的补充,前一个接口保存了数据返回值为空,我们在下一个接口判断数据是否改变,这样一个事务就能去处理一个流程的接口,当然也可以使用数据库数据去校验(有些项目测试人员没有查询线上数据库的权限,所以使用下一接口的判断也是可以的)
因为事务是用户定义的一个操作系列,而HTTP只是每一个操作,所以我们的HTTP请求得写在事务内
现在大家已经了解到了事务控制器,那么我们要开始考虑几个问题了
1、有多少个事务需要执行,怎么去一个事务一个事务的去执行
2、每个事务应该怎么对应需要执行的接口请求
我们需要看到我们csv的一个字段Transaction,用来存放事务名
首先解决第一个问题,多个事务的执行需要用到循环控制器,这样我们可以读取Transaction的数量循环执行下去。
第二个问题,我们需要让事务和事务里面的接口对应起来,这里就需要用到计数器来做下标,逻辑类似代码的for循环,下面直接上图csv
现在我们需要解决最后一个问题,循环控制器的循环次数。
第一个循环控制器的循环次数需要看Transaction有多少个事务。
第二个循环控制器循环次数需要看整个接口数有多少,这样才能读取更完整的数据
首先我们直接在生成token的时候直接先拿到这个csv中我们需要的数据,Transaction的有多少个事务,总接口数有多少个。
代码如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
int pzRowNum=0;
int swRownum = 0;
try
{
BufferedReader br=new BufferedReader(new FileReader("C:/Users/testcase(1).csv"));
String tmpStr="";
String line =br.readLine() ;
while(line!=null)
{
if(!(line.split(",")[0]).isEmpty()){
swRownum++;
}
pzRowNum++;
line = br.readLine();
}
swRownum=swRownum-1;
pzRowNum=pzRowNum-1;
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
vars.put("rowCount",String.valueOf(pzRowNum));
vars.put("swrow",String.valueOf(swRownum));
log.info("事务的总行数:"+String.valueOf(swRownum));
log.info("CSV文件行数:"+String.valueOf(pzRowNum));
现在我们拿到了两个变量,可以在两个循环控制器中使用了,我们也需要加上定时器,很多时候数据库是读写分离,有的接口写入数据后,读取从库时还没有同步数据可能会报错,但是有定时器会解决主库同步数据到从库的时间差
目前为止我们已经把所有的jmeter脚本编写完成了,现在我们开始执行看看结果如何
结果正常执行,到现在我们解决了我们所需要的各种情况
总结
1、其实写完后大家发现,整个逻辑就是把jmeter的插件当做是封装的方法去调用,按照写代码的思维去写就可以写出自己想要的自动化平台。
2、jmeter的bean shell脚本有很多内置方法,大家可以在官方文档里面看各种api,官方文档有些很不明确,有时候就需要看源码,慢慢的琢磨其实jmeter功能真的很强大。
遗留问题
1、看到这里大家已经有了处理接口各种判断的经验,那么多个token需要怎么去做,相信大家已经有一点思路了,继续做判断。
2、如果项目需要自动验证数据去判断是否是生成数据还是测试数据,老规矩在csv中不同环境那个字段的不同去判断,无非是多写几行代码。
3、为什么csv事务可以写在第一行,接口数据不能写在第一行,求大佬们给点思路,我一直没找到原因
后序
接下来我会分享如何介入ant+git+jekins去做持续集成,可以拿到可视化的测试报告和线上监控。
贴上python提取返回值重新写入键值对的代码,你会发现python真的好简洁。
ast = {}
paras = {"name":"testtag"}
#类似二分分类去拿数据重新写入字典,lis就是接口返回值
def func(lis,paras):
if isinstance(lis, list):
for i in lis:
if isinstance(paras,dict):
#在这可以写个try去获取拿不到key的异常就可以了
if paras.items() & ast.items():
break
else:
func(i,paras)
else:
func(i,paras)
elif isinstance(lis, dict):
for k, values in lis.items():
if isinstance(values,list) or isinstance(values,dict):
func(values,paras)
else:
ast[k] = values