接口测试——流量录制回放

摘要:
接口自动化回归技术是我们经常谈到的一种质量保证手段,如今在测试行业针对它的应用已经比较广泛。因此我们就需要一套“流量自动采集录制、回放校验”的工具。Java后端大多数都是采用SpringBoot,因此我们可以使用AOP针对Controller层的拦截来实现流量的录制。在OOP中模块化的关键单元是类,而在AOP中,模块化单元是切面。也就是说AOP关注的不再是类,而是一系列类里面需要共同能力的行为。

接口自动化回归技术是我们经常谈到的一种质量保证手段,如今在测试行业针对它的应用已经比较广泛。对于一个轻量级的系统,当我们想针对它完成一个接口自动化回归工具的时候,我们通常都是通过手动梳理的方法将目前系统应用的对外接口列出来然后,然后查阅接口文档,录入测试用例,最终完成断言,看似是一个完美的解决方案。

但是如果面对磅礴复杂的系统,我们还是采用这样的手段,怕是心有余而力不足。在大型电商网站后台大概有几百个核心应用,成千上万个接口,我们是肯定无法通过手动的方法来完成这些接口的回归用例的编写的。因此我们就需要一种更加智能的方式来完成我们的诉求。因此我们就需要一套“流量自动采集录制、回放校验”的工具。

Java后端大多数都是采用SpringBoot,因此我们可以使用AOP针对Controller层的拦截来实现流量的录制。

首先解释一个AOP:Aspect Oriented Programming 面向切面编程,是 Spring 框架最核心的组件之一,它通过对程序结构的另一种考虑,补充了 OOP(Object-Oriented Programming)面向对象编程。在 OOP 中模块化的关键单元是类,而在 AOP 中,模块化单元是切面。也就是说 AOP 关注的不再是类,而是一系列类里面需要共同能力的行为。

加入我们!群,642830685,领取最新软件测试资料大厂面试和Python自动化、接口、框架搭建学习资料!

假设我们提供了两个对外请求的接口,一个get,一个post:

接口测试——流量录制回放第1张

接口测试——流量录制回放第2张

原始的代码是:

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 classControllerInterceptor {

    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 = newObjectMapper();
    /**
     * 定义拦截规则:拦截com.**.**.controller..)包下面的所有类中,有@RequestMapping注解的方法
     */@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public voidcontrollerMethodPointcut() {
    }

    /**
     * 请求方法前打印内容
     *
     * @param joinPoint
     */@Before("controllerMethodPointcut()")
    public voiddoBefore(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 = newStringBuffer();
        JSONObject header = newJSONObject();
        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 = newStringBuffer();
        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 voiddoAfterReturing(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);

    }
}

加入上面的代码后,我们再次发起请求的,可以看到控制台会输出我们的请求参数,以及服务端的返回。

接口测试——流量录制回放第3张

请求拦截

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造成了影响。

作文不易,如果对你有帮助,点个赞呗!

加入我们!群,642830685,领取最新软件测试资料大厂面试和Python自动化、接口、框架搭建学习资料!

免责声明:文章转载自《接口测试——流量录制回放》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇四、Haproxy的4层及7层IP透传Linux 多个cpp文件的编译(Makefile)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Android开发中java与javascript交互:PhoneGap插件vs addJavascriptInterface

1.前言 在《用PhoneGap+jQueryMobile开发Android应用实例》中,我们讲到PhoneGap(以下称Cordova)开发环境的搭建,以及如何整合出一个基本的Android应用框架(并给出了范例代码)。于是乎,我们便开始日夜兼程,披星戴月的炮制我们的第一个手机应用了。 但实际上,除了常见的API调用规范(有且仅有自查手册一途)引起的问题...

自定义分页控件PageList

usingSystem; usingSystem.Collections; usingSystem.Collections.Generic; usingSystem.Linq; usingSystem.Web; namespaceCommons { public class PageList<T> : IEnumerable<T...

Java Properties 类读取配置文件信息

在我们平时写程序的时候,有些参数是经常改变的,而这种改变不是我们预知的。比如说我们开发了一个操作数据库的模块,在开发的时候我们连接本地的数据库那么IP,数据库名称,表名称,数据库主机等信息是我们本地的,要使得这个操作数据的模块具有通用性,那么以上信息就不能写死在程序里。通常我们的做法是用配置文件来解决。 各种语言都有自己所支持的配置文件类型。比如Pytho...

dubbo(2.5.3)源码之服务消费

消费端启动初始化过程:   消费端的代码解析也是从配置文件解析开始的,服务发布对应的<dubbo:service,解析xml的时候解析了一个ServiceBean,并且调用ServiceConfig进行服务的发布。服务的消费对应的<dubbo:reference,在初始化的过程中也解析了一个 ReferenceBean类去做处理。在bean加...

Flask-SQLAlchemy操作

Flask-SQLAlchemy SQLAlchemy 一. 介绍 SQLAlchemy是一个基于Python实现的ORM框架。该框架建立在 DB API之上,使用关系对象映射进行数据库操作,简言之便是:将类和对象转换成SQL,然后使用数据API执行SQL并获取执行结果。 pip3 install sqlalchemy 组成部分: Engine,框架...

Node.js源码初探~我很好奇

前言: 最近在看Node.js,看了一段时间后便想着看看Node.js源码,自己本地调试调试;现在便说说这个过程中的坑,以及一些需要注意的地方;       Node.js需要一定C++基础,建议看完C++Primer再看,否则V8的好多表达方式,指针,引用,模板之类的会看不懂;       代码已上传GitHub地址:   https://github....