前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2021第一篇-流量录制回放完整案例

2021第一篇-流量录制回放完整案例

作者头像
Antony
修改2021-02-22 22:16:30
2K0
修改2021-02-22 22:16:30
举报
文章被收录于专栏:软件测试那些事

新年第一篇,祝大家牛年大吉,牛气冲天。

在之前的《录制回放实现测试用例自由》一文中,笔者简单介绍了如何通过切面来录制HTTP接口请求和返回,并实现了用例的回放。 当然,在实际的项目中,对于应用来说,除了来自于前端的HTTP访问请求之外,至少还会有一个数据库,或者对于其他应用的服务间调用。仅仅对HTTP接口进行录制,而不去控制数据库等依赖进行控制的话,并不能保证录制出来的用例有足够的稳定性。 本文中,我们将进一步实现一个更为完整的解决方案。

录制

在哪里录制?

假设我们是在一个运行的应用中进行录制。通过一个配置可以控制录制功能的开关,默认为关闭。 那我们的测试工具也得作为应用的一部分部署到线上去,而不仅仅是测试阶段。这可能需要说服团队来接受。当然,如果是将此功能作为日志或者审计等通用微服务框架组件的增值功能,可以提高团队接受的可能性。 当然,阿里也开源了JVM-SandBox这一利器,可以动态挂载来实现拦截,感兴趣的同学也可以利用它来实现无侵入的方案。

如何获取到录制的文件?

做过系统测试的覆盖率统计的同学都知道Jacoco提供了JacocoAgent来实现覆盖率统计。运行时通过指定javaagent的方式进行挂载。

代码语言:javascript
复制
java -javaagent:/tmp/jacoco/lib/jacocoagent.jar=includes=*, 
output=tcpserver,port=6300,address=localhost, 
append=true -jar demo-1.0-SNAPSHOT.jar

通过这行命令,可以在启动jacocoagent并进行插桩的同时,启动一个tcpserver,外部使用者可以利用maven-jacoco-plugin等工具来dump获取到覆盖率报告。 考虑通过HTTP API接口触发dump, 可以落在系统指定路径下,按照时间戳分别命名为xxxx_request.json和xxxx_record.json,分别表示http请求以及服务间依赖(db)的数据。 类似的,我们考虑通过HTTP API接口触发dump,并将录制的数据落地到日志文件,供后续使用。 这样,我们的使用场景就变成了

  • 测试人员在前台操作,或者其它服务调用了被测服务
  • 录制被测服务的请求/返回以及外部服务调用的请求/返回
  • 通过调用dump接口来落地成记录文件
  • 验证测试文件正确,并纳入测试用例库
  • 通过测试框架来运行测试文件,执行用例。

dump接口

代码语言:javascript
复制
package io.metersphere.aspect.controller;

import io.metersphere.aspect.service.DumpService;
import io.metersphere.controller.ResultHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;

@RestController
@RequestMapping
public class DumpController {

    @Resource
    private DumpService dumpService;
     @GetMapping(value = "/dump")
    public ResultHolder dump(@RequestParam Boolean append) {
        String enabled = System.getProperty("dump","TRUE");
        if (StringUtils.equalsIgnoreCase("true", enabled)) {
             dumpService.execute(append);
            return ResultHolder.success("called");
        }
        return ResultHolder.success("");
    }

}

假定默认是启用录制的情况,还需要一个DumpService来落地文件

代码语言:javascript
复制
package io.metersphere.aspect.service;

import com.google.common.io.Files;
import com.google.gson.Gson;
import io.metersphere.aspect.DBMapperIntercept;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.UUID;

@Service
@Transactional(rollbackFor = Exception.class)
public class DumpService {

    @Resource
    private DBMapperIntercept dbMapperIntercept;

    private int sequence=1;
    private String REQUEST = "request";
    private String RECORD="record";

    public void execute(Boolean append) {
        String uuid=UUID.randomUUID().toString();
        String appeendix=sequence+"_"+uuid+".json";
        Gson gson = new Gson();
        String dbRecords=gson.toJson(DBMapperIntercept.records);
        String requests=gson.toJson(DBMapperIntercept.requests);
        String fileName=RECORD+"_"+appeendix;
        String requestName=REQUEST+"_"+appeendix;;
        File dbfile= new File(fileName);
        File requestfile= new File(requestName);
        try {
            Files.write(dbRecords.getBytes(),dbfile);
            Files.write(requests.getBytes(),requestfile);
            sequence++;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

录制案例

这样,只要在应用启动,就可以启动录制了。有如下场景

代码语言:javascript
复制
package io.metersphere.aspect.test;

import com.alibaba.fastjson.JSON;
import io.metersphere.TestApp;
import io.metersphere.controller.request.LoginRequest;
import org.junit.jupiter.api.*;

public class DumpControllerTest extends TestApp {

    @Test
    public void loginSetup() throws Exception {
        LoginRequest loginRequest= new LoginRequest();
        loginRequest.setUsername("admin");
        loginRequest.setPassword("metersphere");
        doPost("/signin",JSON.toJSONString(loginRequest));
    }
    @AfterEach
    public void testDump() throws Exception {
       doGet("/dump?append=false");
     }
}

在这个示例中,我们首先完成了登录用例,然后在@AfterEach中调用了dump接口来触发服务端完成录制结果的落地。 当然这只是一个用自动化测试用例来展示的demo。实际的项目中,可以是测试人员在前端触发接口调用,并在一个场景完成后,通过postman等工具来实现完成录制结果的dump。

如果后期考虑和用例管理平台或者devops平台进行集成,直接生成用例并纳入用例库,则可以在dump接口中直接返回录制结果。

如何执行用例?-单个用例执行-集成测试场景

在这里展示一个简单的场景,即通过在用例上注解指明request 和record的文件名称,测试框架将自动解析文件并执行用例(集成测试、MockMvc场景下) 笔者之前也写过另外一篇文章《如何用Junit5玩出参数化测试的新花样?》介绍如何将整个目录作为用例集来执行,感兴趣的读者也可以了解下。这样,就不再需要去编写一个个脚手架用例了。

代码语言:javascript
复制
@Slf4j
@ExtendWith(ScenarioExtension.class)
public class ScenarioPlayTest extends TestBase {
    @Scenario(request ="src/test/resources/dbmock/request.json",value="src/test/resources/dbmock/record.json")
    public void testAddProject() throws Exception {
    }
}

这里的request/value代表了HTTP请求和服务调用。 为了实现@Scenario这一效果,我们来编写一个ScenarioExtension注解。其逻辑为: 1)判断测试用例中是否存在@Scenario注解 2)如果存在则判断是否存在服务调用文件,即value,存在则通过它来提供依赖服务的测试桩 3)读取request文件,并调用执行器Runner来执行。这里我们使用了MockMvcRunner,也就是通过MockMVC来执行HTTP请求。

代码语言:javascript
复制
package io.metersphere.aspect.junit5;

import java.io.File;
import java.util.List;
import com.alibaba.fastjson.JSON;
import io.metersphere.aspect.MapperRecord;
import io.metersphere.aspect.MockDBUtils;
import io.metersphere.aspect.Modes;
import io.metersphere.aspect.junit5.runner.MockMvcRunner;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@Component
public class ScenarioExtension
        implements BeforeTestExecutionCallback {

    protected MockMvcRunner runner;

    // EXTENSION POINTS
    public void beforeTestExecution(ExtensionContext context) throws Exception{
        // get the dataSource bean from the spring context
        ApplicationContext springContext = SpringExtension.getApplicationContext(context);
        runner=springContext.getBean(MockMvcRunner.class);
        Scenario scenario = AnnotationUtils.findAnnotation(context.getRequiredTestMethod(), Scenario.class).orElse(null);
        if (scenario == null) {
            scenario = AnnotationUtils.findAnnotation(context.getRequiredTestClass(), Scenario.class).orElse(null);
        }
        String request=scenario.request();
        File requestFile= new File(request);
        String filename = scenario.value();
        File recordFile= new File(filename);
        if(recordFile.exists()){ //提供依赖服务
            MockDBUtils.setMode(Modes.SIM);
            MockDBUtils.provideStubs(filename);
        }
        if(requestFile.exists()) {
            //文件不存在,不执行用例
            List<MapperRecord> recordList = MockDBUtils.getMapperRecords(request);
            assertThat(recordList).isNotEmpty();
            for (MapperRecord record : recordList
            ) {
                runner.run(record);
                // assertThatJson(result).when(Option.IGNORING_EXTRA_FIELDS).isEqualTo(record.getReturning());
            }
        }
    }
}

@Scenario的注解如下,

代码语言:javascript
复制
package io.metersphere.aspect.junit5;



import java.lang.annotation.*;


import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(ScenarioExtension.class)
@Test
public @interface Scenario {

  String  value() default "";
  String request() default "";
}

这个注解可以在类或者方法上使用,目前不支持叠加。另外,额外带了@Test注解,在测试用例中就可以不再标注了。 至于MockMVCRunner,则是对于MockMvc中调用HTTP GET/POST请求的简单封装,限于篇幅就不再展示源码了。感兴趣的读者可以参考笔者的另外一篇文章《MockMvc -你需要一个测试基类》来了解更多。

Mapper拦截

在之前的文章中,笔者已经介绍了对Controller接口的拦截。在原先的基础上,我们需要额外增加对于服务间依赖调用的拦截,此处以数据库为例。

代码语言:javascript
复制
package io.metersphere.aspect;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.util.stream.Collectors.joining;


/**
 *
 * @author Antony
 * 通过aop拦截后执行具体操作
 */
@Aspect
@Order(9)
@Component
@Slf4j
public class DBMapperIntercept {
    public static  List<MapperRecord> records=new ArrayList<>();
    public static  List<MapperRecord> requests=new ArrayList<>();
    MapperRecord requestRecord = new MapperRecord();
    public static Modes mode=Modes.REC;
  @Pointcut("execution(public * io.metersphere.base.mapper..*.*(..))")
    public void recordLog(){}

    @Around("recordLog()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //1.获取className+methdoName+args
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        String className=pjp.getSignature().getDeclaringType().getSimpleName();

        String methodName = pjp.getSignature().getName();
        Object[] args=pjp.getArgs();
        Type returnType =signature.getMethod().getAnnotatedReturnType().getType();

        MapperRecord mapperRecord = new MapperRecord();
        mapperRecord.setClassName(className);
        mapperRecord.setMethodName(methodName);
        mapperRecord.setArgs(args);

        //进入播放模式
        if(mode.equals(Modes.SIM)){
            MapperRecord record=MockDBUtils.getMockData(records,mapperRecord);
            MockDBUtils.print(record);
            if(record==null) { return null; }
            Object data=record.getReturning();
            return GsonUtil.fromJson(data,returnType);
        }
        //2.执行程序并获取结果
        Object proceed = pjp.proceed();
        mapperRecord.setReturning(proceed);
        //保存记录
        if(mode.equals(Modes.REC)) {
            records.add(mapperRecord);
        }
        return proceed;
    }
}

我们利用切面获取到了Mapper调用的类、方法、入参和返回,并且将这些记录保存进了 List<MapperRecord> requests, 供dump时导出使用。 在原先录制Controller方法的基础上,再叠加对Mapper的拦截,我们就具备了录制请求和(数据库)依赖的能力。

总结

通过上述实践,我们可以按照如下的方式进行测试用例的开发和执行 1)启用录制功能,对应用的请求和依赖进行录制,形成测试用例(文件) 2)将录制的文件作为测试用例来执行(集成测试) 涉及的技术点

  • @Aspect
  • Junit5 Extension
  • MockMvc

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 软件测试那些事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 录制
    • 在哪里录制?
      • 如何获取到录制的文件?
        • dump接口
          • 录制案例
          • 如何执行用例?-单个用例执行-集成测试场景
          • Mapper拦截
          • 总结
          相关产品与服务
          数据库
          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档