流量录制回放助力接口自动化测试
◆版权声明:本文出自胖喵~的博客,转载必须注明出处。
转载请注明出处:https://www.cnblogs.com/by-dream/p/13027446.html
接口自动化回归技术是我们经常谈到的一种质量保证手段,如今在测试行业针对它的应用已经比较广泛。对于一个轻量级的系统,当我们想针对它完成一个接口自动化回归工具的时候,我们通常都是通过手动梳理的方法将目前系统应用的对外接口列出来然后,然后查阅接口文档,录入测试用例,最终完成断言,看似是一个完美的解决方案。
但是如果面对磅礴复杂的系统,我们还是采用这样的手段,怕是心有余而力不足。在大型电商网站后台大概有几百个核心应用,成千上万个接口,我们是肯定无法通过手动的方法来完成这些接口的回归用例的编写的。因此我们就需要一种更加智能的方式来完成我们的诉求。因此我们就需要一套“流量自动采集录制、回放校验”的工具。
Java后端大多数都是采用SpringBoot,因此我们可以使用AOP针对Controller层的拦截来实现流量的录制。
首先解释一个AOP:Aspect Oriented Programming 面向切面编程,是 Spring 框架最核心的组件之一,它通过对程序结构的另一种考虑,补充了 OOP(Object-Oriented Programming)面向对象编程。在 OOP 中模块化的关键单元是类,而在 AOP 中,模块化单元是切面。也就是说 AOP 关注的不再是类,而是一系列类里面需要共同能力的行为。
假设我们提供了两个对外请求的接口,一个get,一个post:
原始的代码是:
package com.aop.demo.controller; import com.alibaba.fastjson.JSONObject; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class test { // 测试get请求 @RequestMapping("/testget") JSONObject testGet(int age, String name) { JSONObject object = new JSONObject(); object.put("age", age); object.put("name", name); object.put("time", System.currentTimeMillis()); System.out.println("get请求"); return object; } // 测试post请求 @RequestMapping("/testpost") JSONObject testPost(@RequestBody JSONObject object) { object.put("time", System.currentTimeMillis()); System.out.println("post请求"); return object; } }
我们加入AOP的代码,对controller层进行拦截。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
package com.aop.demo.aop; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.Serializable; import java.util.Enumeration; import java.util.UUID; @Aspect @Component public class ControllerInterceptor { private static final Logger log = LoggerFactory.getLogger(ControllerInterceptor.class); private static ThreadLocal<Long> startTime = new ThreadLocal<Long>(); private static ThreadLocal<String> key = new ThreadLocal<String>(); private static ObjectMapper objectMapper = new ObjectMapper(); /** * 定义拦截规则:拦截com.**.**.controller..)包下面的所有类中,有@RequestMapping注解的方法 */ @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public void controllerMethodPointcut() { } /** * 请求方法前打印内容 * * @param joinPoint */ @Before("controllerMethodPointcut()") public void doBefore(JoinPoint joinPoint) { // 请求开始时间 startTime.set(System.currentTimeMillis()); // 上下文的Request容器 RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); // 获取请求头 Enumeration<String> enumeration = request.getHeaderNames(); StringBuffer headers = new StringBuffer(); JSONObject header = new JSONObject(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); String value = request.getHeader(name); headers.append(name + ":" + value).append(","); header.put(name, value); } // uri String uri = UUID.randomUUID() +"_"+ request.getRequestURI(); // 获取param String method = request.getMethod(); StringBuffer params = new StringBuffer(); if (HttpMethod.GET.toString().equals(method)) {// get请求 String queryString = request.getQueryString(); if (queryString !=null && !queryString.isEmpty()) { //params.append(URLEncodedUtils.encode(queryString, "UTF-8")); params.append(queryString); } } else {//其他请求 Object[] paramsArray = joinPoint.getArgs(); if (paramsArray != null && paramsArray.length > 0) { for (int i = 0; i < paramsArray.length; i++) { if (paramsArray[i] instanceof Serializable) { params.append(paramsArray[i].toString()).append(","); } else { //使用json序列化 反射等等方法 反序列化会影响请求性能建议重写tostring方法实现系列化接口 try { String param= objectMapper.writeValueAsString(paramsArray[i]); if(param !=null && !param.isEmpty()) params.append(param).append(","); } catch (JsonProcessingException e) { log.error("doBefore obj to json exception obj={},msg={}",paramsArray[i],e); } } } } } key.set(uri); System.out.println("请求拦截 uri:"+ uri+ " method:"+ method+ " params:"+params+ " headers:"+ headers); } /** * 在方法执行后打印返回内容 * * @param obj */ @AfterReturning(returning = "obj", pointcut = "controllerMethodPointcut()") public void doAfterReturing(Object obj) { long costTime = System.currentTimeMillis() - startTime.get(); String uri = key.get(); startTime.remove(); key.remove(); String result= null; if(obj instanceof Serializable){ result = obj.toString(); }else { if(obj != null) { try { result = objectMapper.writeValueAsString(obj); } catch (JsonProcessingException e) { log.error("doAfterReturing obj to json exception obj={},msg={}",obj,e); } } } System.out.println("结果拦截 uri:"+ uri+ " result:" +result+ " costTime:"+costTime); } }
加入上面的代码后,我们再次发起请求的,可以看到控制台会输出我们的请求参数,以及服务端的返回。
请求拦截
uri:e1e58662-0a7b-4433-bf7b-d8cd6207e9df_/testpost
method:POST
params:{"address":"Beijing","name":"tom","age":19},
headers:
host:127.0.0.1:8844,
connection:keep-alive,
content-length:50,
postman-token:dc0fbeb8-eb19-dcf2-7233-2ce7397db7f6,
cache-control:no-cache,
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36,
content-type:application/json,
accept:*/*,origin:chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop,
sec-fetch-site:none,
sec-fetch-mode:cors,
sec-fetch-dest:empty,
accept-encoding:gzip, deflate, br,
accept-language:zh-CN,zh;q=0.9,
结果拦截
uri:e1e58662-0a7b-4433-bf7b-d8cd6207e9df_/testpost
result:{"address":"Beijing","name":"tom","time":1591016171226,"age":19}
costTime:0
我们可以看到整个过程的请求url、参数、header都被我们记录了下来,我们只需拿到这些就可以做为我们回归的入参,而使用这些入参请求拿到结果后,可以和我们拦截的结果数据进行对比,这样可以判断我们当前回放的这一次请求的返回结果是否符合我们的预期。
如果我们的整个研发环境是既有线上环境和有又测试环境的话,通常我们会采集线上环境的数据,做为用例,此时在迭代过程中的分支也就是测试环境中进行用例的回放和结果的对比,这样就可以知道我们在迭代过程中,是否对线上目前已有的case造成了影响。