专栏首页软件测试那些事2021第一篇-流量录制回放完整案例

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

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

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

录制

在哪里录制?

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

如何获取到录制的文件?

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

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接口

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来落地文件

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();
        }
    }
}

录制案例

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

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玩出参数化测试的新花样?》介绍如何将整个目录作为用例集来执行,感兴趣的读者也可以了解下。这样,就不再需要去编写一个个脚手架用例了。

@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请求。

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的注解如下,

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接口的拦截。在原先的基础上,我们需要额外增加对于服务间依赖调用的拦截,此处以数据库为例。

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

本文分享自微信公众号 - 软件测试那些事(antony-not-available),作者:风月同天测试人

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-01-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何用Junit5玩出参数化测试的新花样?

    这是之前一篇文章《用junit5编写一个类ZeroCode的测试框架》的续集。主要将在之前工作的基础上,围绕参数化测试展开。 框架主要设计点:

    Antony
  • 看,Mockito如何搞定Builder模式的Fluent API

    建造者模式Builder是一种常用的设计模式,用于构建不同的产品类。 如有以下的Builder

    Antony
  • MeterSphere单元测试-Mockito-Inline出场

    在之前的测试旅程中,我们新建了测试计划并将测试用例纳入该计划来执行。以下是上述用例执行之后对添加测试计划的一个代码覆盖率。

    Antony
  • 你能体会那种写 Python 时不用 import 的幸福吗?

    我们有时候写着写着发现需要引入新的库,就又得回到前面,再 import 一波,如果你用的是类似 jupyter 的编辑器,你添加完 import 语句之后还得再...

    小小詹同学
  • SpringMVC当中请给出一个下载的例子,文件名必需是中文

    4.文件下载 例4.1: <%@ page contentType="text/html; charset=GBK" %> <html> <body > <A ...

    马克java社区
  • 【玩转腾讯云】一次jpa自定义查询方法的使用尝试过程

    目前客户有一个需求:每一个用户想要看到的帖子顺序都不一样,用户可以按照自己的喜好排列帖子顺序,并且可以手动把某个帖子置顶显示。

    Ezio4396
  • Python爬虫七麦APP榜单

    week
  • 如何用Junit5玩出参数化测试的新花样?

    这是之前一篇文章《用junit5编写一个类ZeroCode的测试框架》的续集。主要将在之前工作的基础上,围绕参数化测试展开。 框架主要设计点:

    Criss@陈磊
  • python 实现elk接口获取数据

    [root@ctum2A0703016 ~]# cat jiaoyihao.py #!/usr/bin/python2.7

    py3study
  • 如何用Junit5玩出参数化测试的新花样?

    这是之前一篇文章《用junit5编写一个类ZeroCode的测试框架》的续集。主要将在之前工作的基础上,围绕参数化测试展开。 框架主要设计点:

    Antony

扫码关注云+社区

领取腾讯云代金券