jmeter+csv写一个模板化的接口自动化平台

一、目的:

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

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值