关闭

Java单元测试,最好用的mock框架是什么?

发表于:2023-7-24 09:33

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:AREX中文社区    来源:知乎

  AREX 是一款开源的基于真实请求与数据的自动化回归测试平台,利用 Java Agent 字节码注入技术,通过在生产环境录制和存储请求、应答数据,并在测试环境回放请求和注入 Mock 数据,存储新的应答,实现了自动录制、自动回放、自动比对,为接口回归测试提供便利。通过巧妙创新的 Mock 机制,可实现写流量的回归验证。
  AREX Mock 功能十分强大,不仅支持各种主流技术框架的自动数据采集和 Mock,还支持了本地时间、缓存数据以及各种内存数据的采集和 Mock,可以做到在回放时精准还原生产执行时的数据环境,且不会产生脏数据。
  以下将从代码实现的角度简单介绍下 AREX 是如何实现在流量回放时自动 Mock 数据的。
  示例
  让我们先以一个简单的函数为例,理解?下其实现原理。假定我们有下面?个函数,用于将给定的 IP 字符串转换成整型,代码如下:
  public Integer parseIp(String ip) {
      int result = 0;
      if (checkFormat(ip)) { // 检查IP串是否合法
          String[] ipArray = ip.split("\\.");
          for (int i = 0; i < ipArray.length; i++) {
              result = result << 8;
              result += Integer.parseInt(ipArray[i]);
          }
      }
      return result;
  }
  我们将从两个方面说明如何实现该函数的流量回放功能:
  · Record(流量采集)
  当这个函数被调用时,我们把对应的请求参数和返回结果保存下来,供后面流量回放使用,代码如下:
  if (needRecord()) {
      // 数据采集,将参数和执?结果保存进DB
      DataService.save("parseIp", ip, result);
  }
  · Replay(流量回放)
  在进行流量回放时,就可以用之前采集的数据来自动实现这个函数的 Mock,代码如下:
  if (needReplay()) {
      return DataService.query("parseIp", ip);
  }
  通过查看完整的代码,我们可以更好地理解其实现逻辑:
  public Integer parseIp(String ip) {
      if (needReplay()) {
          // 回放的场景,使?采集的数据做为返回结果,也就是 Mock
          return DataService.query("parseIp", ip);
      }
   
      int result = 0;
      if (checkFormat(ip)) {
          String[] ipArray = ip.split("\\.");
          for (int i = 0; i < ipArray.length; i++) {
              result = result << 8;
              result += Integer.parseInt(ipArray[i]);
          }
      }
   
      if (needRecord()) {
          // 录制的场景,将参数和执?结果保存进到数据库
          DataService.save("pareseIp", ip, result);
      }
      return result;
  }
  AREX 中的具体实现
  AREX 实现的原理类似,不过会更复杂?些,不需要开发人员手动在业务代码中添加录制和回放的代码。arex-agent 会在应用启动时,在需要的代码块中自动添加相应的代码来实现这个功能。这里以 MyBatis3 的 Query 为例,看看 AREX 中的具体实现。
  阅读过 MyBatis 源码的应该都了解,Query 的操作都会收束在 org.apache.ibatis.executor.BaseExecutor 类的 query 方法上(Batch 操作除外),这个方法的签名如下:
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException
  这?包含了执行的 SQL 和参数,函数的结果包含了从数据库中查到的数据,显然在这里执行数据采集是合适的,在回放的时候也可以用采集的数据作为结果返回,从而避免实际的数据库操作。看看 AREX 中的代码,为了便于理解,这里做了?定的简化,如下:
  public class ExecutorInstrumentation extends TypeInstrumentation {
      @Override
      protected ElementMatcher<TypeDescription> typeMatcher() {
          // 需要进行代码注入的类全名
          return named("org.apache.ibatis.executor.BaseExecutor");
      }
   
      @Override
      public List<MethodInstrumentation> methodAdvices() {
          // 需要进行代码注入的方法名,因为query方法存在多个重载,所以带上了参数验证
          return Collections.singletonList(new MethodInstrumentation(
                          named("query").and(isPublic())
                                  .and(takesArguments(6))
                                  .and(takesArgument(0, named("org.apache.ibatis.mapping.MappedStatement")))
                                  .and(takesArgument(1, Object.class))
                                  .and(takesArgument(5, named("org.apache.ibatis.mapping.BoundSql"))),
                          QueryAdvice.class.getName())
          );
      }
   
      // 注入的代码
      public static class QueryAdvice {
   
          @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class)
          public static boolean onMethodEnter(@Advice.Argument(0) MappedStatement var1,
                                              @Advice.Argument(1) Object var2,
                                              @Advice.Argument(5) BoundSql boundSql,
                                              @Advice.Local("mockResult") MockResult mockResult) {
              RepeatedCollectManager.enter(); // 防止嵌套调用导致的数据重复采集
              if (ContextManager.needReplay()) {
                  mockResult = InternalExecutor.replay(var1, var2, boundSql, "query");
              }
              return mockResult != null;
          }
   
          @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
          public static void onMethodExit(@Advice.Argument(0) MappedStatement var1,
                                    @Advice.Argument(1) Object var2,
                                    @Advice.Argument(5) BoundSql boundSql,
                                    @Advice.Thrown(readOnly = false) Throwable throwable,
                                    @Advice.Return(readOnly = false) List<?> result,
                                    @Advice.Local("mockResult") MockResult mockResult) {
              if (!RepeatedCollectManager.exitAndValidate()) {
                  return;
              }
   
              if (mockResult != null) {
                  if (mockResult.getThrowable() != null) {
                      throwable = mockResult.getThrowable();
                  } else {
                      result = (List<?>) mockResult.getResult();
                  }
                  return;
              }           
   
              if (ContextManager.needRecord()) {
                  InternalExecutor.record(var1, var2, boundSql, result, throwable, "query");
              }
          }
      }
  }
  其中 QueryAdvice 是需要在 query 方法中注入的代码。通过 onMethodEnter 注入的代码会在方法最开始地位置执行,而 onMethodExit 注入的代码则会在函数返回结果之前执行。
  单纯地看这个可能比较难于理解,我们把注入代码后的 BaseExecutor 的 query 方法的代码 dump下来进行分析,如下:
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          MockResult mockResult = null;
          boolean skipOk;
          try {
              RepeatedCollectManager.enter();
              if (ContextManager.needReplay()) {
                  mockResult = InternalExecutor.replay(ms, parameter, boundSql, "query");
              }
   
              skipOk = mockResult != null;
          } catch (Throwable var28) {
              var28.printStackTrace();
              skipOk = false;
          }
   
          List result;
          Throwable throwable;
          if (skipOk) {
              // 重放的场景,不再执行原来的 query 方法体
              result = null;
          } else {
              try {
                  // BaseExecutor query 方法的原代码,此处省略,唯一会被调整的就是原方法里 return 的代码,会被修改为将结果赋值给 result
                  result = list;
              } catch (Throwable var27) {
                  throwable = var27;
                  result = null;
              }
          }
   
          try {
              if (mockResult != null) {
                  if (mockResult.getThrowable() != null) {
                      throwable = mockResult.getThrowable();
                  } else {
                      result = (List)mockResult.getResult();
                  }
              } else if (RepeatedCollectManager.exitAndValidate() && ContextManager.needRecord()) {
                  InternalExecutor.record(ms, parameter, boundSql, result, throwable, "query");
              }
          } catch (Throwable var26) {
              var26.printStackTrace();
          }
   
          if (throwable != null) {
              throw throwable;
          } else {
              return result;
          }
      }
  可以看到 onMethodEnter 和 onMethodExit 里的代码被插?到了开头和结尾,再来理解下这段代码:
  · 录制的场景
  AREX 会判断这次访问数据是否需要录制(服务收到请求时,AREX 会根据配置的录制频率决定是否对这个请求进行录制,如果判断为需要录制,则这个请求执行过程中所有的外部依赖都会被录制,具体实现细节这里不做介绍了)。录制过程中,AREX 会调用 InternalExecutor.record(ms, parameter, boundSql, result, throwable, "query")方法,将本次数据库访问的结果、核心参数等信息存入AREX的数据库中,完成对该数据库访问的录制。
  · 回放的场景
  从上面的代码可以看到,当把前面录制的请求再次发送给对应服务时,AREX 会将其视为回放,此时不会再执行原函数的代码了,而是直接返回之前录制下来的结果(包括当时异常的还原),通过调用 InternalExecutor.replay(ms, parameter, boundSql, "query”) 可以获取之前保存的录制数据。
  内存数据的 Record&Replay(动态类)
  当然,前面示例的函数是幂等的,对于幂等函数而言,由于每次调用时,其返回结果始终相同,不会受到外部因素的影响,因此在录制和回放过程中并不需要进行数据的采集和 Mock。
  相反,对于非幂等的函数,每次调用的结果可能会受到外部环境的影响,并且执行结果会影响服务输出(例如各种本地缓存,不同的环境数据可能不同,从而影响输出结果)。在这种情况下,AREX 也提供配置动态类这种机制来实现这部分数据的 Record 和 Mock 功能,具体可以在 Setting 子菜单的 Record 配置项中配置:
  在这里依次配置类名、方法名(非必需,不配置的话将会应用于所有有参数和返回值的公共方法)、参数类型(非必需)。配置完成后,arex-agent 将会自动在对应的方法中注入类似上面的 Record&Replay 代码,从而实现数据的采集和回放时的 Mock 功能。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号