流量錄製回放助力介面自動化測試
◆版權聲明:本文出自胖喵~的部落格,轉載必須註明出處。
轉載請註明出處://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造成了影響。