自动化回归测试平台 AREX 的 Mock 实现原理
AREX 是一款开源的基于真实请求与数据的自动化回归测试平台,利用 Java Agent 字节码注入技术,通过在生产环境录制和存储请求、应答数据,并在测试环境回放请求和注入 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 功能。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
iOS 单元测试之常用框架 OCMock 详解 | 京东云技术团队
一、单元测试 1.1 单元测试的必要性 测试驱动开发并不是一个很新鲜的概念了。在日常开发中,很多时候需要测试,但是这种输出是必须在点击一系列按钮之后才能在屏幕上显示出来的东西。测试的时候,往往是用模拟器一次一次的从头开始启动 app,然后定位到自己所在模块的程序,做一系列的点击操作,然后查看结果是否符合自己预期。 这种行为无疑是对时间的巨大浪费。于是有很多资深工程师们发现,我们是可以在代码中构造一个类似的场景,然后在代码中调用我们之前想要检查的代码,并将运行结果和设想结果在程序中进行比较,如果一致,则说明我们的代码没有问题,由此就产生了单元测试。 1.2 单元测试的目的 单元测试的主要目的是发现模块内部逻辑、语法、算法和功能错误。 单元测试主要是基于白盒测试验证以下问题: 验证代码与设计相符度。 发现设计和需求中存在错误。 发现在编码过程中引入的错误。 单元测试关注的重点有以下部分: **独立路径-**对于基本执行路径和循环进行测试,可能的错误有: 不同数据类型的比较。 “差1错”,即可能多循环或少循环一次。 错误或不可能的终止条件。 不适当的修改了循环变量。 **局部数据结构-**单...
- 下一篇
Python自动化测试的配置层实现方式对标与落地 | 京东云技术团队
Python中什么是配置文件,配置文件如何使用,有哪些支持的配置文件等内容,话不多说,让我们一起看看吧~ 1 什么是配置文件? 配置文件是用于配置计算机程序的参数和初始化设置的文件,如果没有这些配置程序可能无法运行或是影响运行(运行速度、便捷性等),使用配置文件的好处在于,部分内容以及环境运行时只需要修改配置文件的参数内容,而无需去代码里查找并修改,提高便捷性、提高可维护性。 2 配置文件有哪几种? 配置主要有四种形式: 第一种是YAML、JSON、XML、TOML、INI、Linux系统中的.bashrc一类,主要应用于软件测试领域,在软件测试的领域行业中,大多数公司采用最新的YAML形式来作为配置文件,例如数据库地址、用例数据等内容的存放,而少部分公司仍然采用旧的INI配置形式 第二种是excel表格的形式,在excel表格中会有固定的title代表每个字段列的含义,有多列,以此来进行配置,多用于游戏领域中,在游戏行业大量使用excel表格的形式,已经是一个常态了。 第三种是py文件,py文件对于一个纯Python项目而言是非常方便的,它不需要做数据的读取操作,只需要进行导入即可,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果