@HoverflyCapture
在之前的文章《真香系列之1-Hoverfly服务虚拟化,你不2的选择》中简单介绍了Hoverfly。本文将介绍如何在JUnit5中使用Hoverfly,并讨论入参匹配、延迟、特性增强等话题。
利用Hoverfly作为HTTP代理的原理,可以非常轻松地将HTTP网络流量抓取下来进行录制并保存为文件。有如下来自官网的案例,
@HoverflyCapture(path = "src/test/resources/hoverfly",
filename = "captured-simulation.json",
config = @HoverflyConfig(captureAllHeaders = true,
proxyLocalHost = true))
@ExtendWith(HoverflyExtension.class)
class CaptureTests {
// ...
}
这其中,如果不写path的话,将会默认保存到"src/test/resources/hoverfly"目录下。实际项目中,可以考虑将录制的文件保存到与测试类相同的包内,以便于查找和维护关联关系。
这里要注意的是,默认情况下处于录制状态时,如果指定文件已经存在,Hoverfly将会直接将文件内容根据本次录制的结果进行覆盖,而不是在文末进行增补。
@HoverflySimulate
录制得来或者自行编写的模拟文件可以用来回放,
@HoverflySimulate(source = @HoverflySimulate.Source(
value = "test-service-https.json",
type = HoverflySimulate.SourceType.CLASSPATH))
@ExtendWith(HoverflyExtension.class)
class SimulationTests {
// ...
}
其中的source用于指定模拟文件名称和类型。
enum SourceType {
DEFAULT_PATH, //一般是"src/test/resources/hoverfly"
CLASSPATH,
URL,
FILE
}
CASSPATH, URL, FILE}
其中使用比较多得的是CLASSPATH或者是FILE。
enableAutoCapture
如前一小节中所介绍的,Hoverfly相对于一般的API模拟工具一个易用性提升的地方就是支持自动的录制加回放。只需要在@HoverflySimulate的注解中额外增加一个配置即可。
@HoverflySimulate(source = @Source(
value = simulation.json", type = SourceType.FILE),
enableAutoCapture = true)
配置项enableAutoCapture 为true的话,就会在用例执行前判断
1)如果指定文件不存在,则当前为录制状态,Hoverfly将记录往来流量,并在指定目录保存为指定文件。
2)如果指定文件存在,则认为当前处于回放状态。Hoverfly将根据该文件提供的内容进行回放。
处于模拟状态时,如果发生了用例中的请求与模拟文件中的任一请求均不匹配的,Hoverfly也不会再将请求转发给真实的目标,而是直接抛出无法匹配的异常。
另外,Hoverfly还提供了Diff、Spy、Synthesize、Modify等模式。详细的各个模式介绍可以参见刘冉的《软件测试中的服务虚拟化(Service Virtualization)》一文
以下是笔者整理的一个Hoverfly工作模式简表,可以看到JUnit5或者Junit4目前只是提供了一部分Hoverfly的功能。
介绍完了Hoverfly在Junit5中的基本使用,再就几个实践中遇到的话题简单介绍一下。
参数匹配
Hoverfly支持三种简单的参数匹配模式,分别是精确匹配(Exact)、模糊匹配(Glob)和正则匹配(Regex)
默认情况下,Hoverfly采用的精确匹配,如下例
"path": [ {
"matcher": "exact",
"value": "/api/bookings/1" }],
"method": [ {
"matcher": "exact",
"value": "GET" }],
这是对某个HTTP接口请求的模拟案例,业务含义为查询ID为1的书籍信息。如果模拟的接口的入参从1变成了2,也就是path的值变成了“/api/bookings/2”,或者是请求方法从GET变成了POST,那么Hoverfly就会认为是匹配失败,而不再使用上述模拟数据了。
在某些场景中,如果希望无论”/api/bookings”这个接口所附带的参数是什么,也就是客户端无论发送查询什么书籍的请求,都希望可以匹配并返回相同的信息。这就需要使用到模糊匹配了。”path”部分修改如下,
"path": [ {
"matcher": "glob",
"value": "/api/bookings/*" }],
将”matcher”从”exact”修改为”glob”,同时在”value”中通过”*”来通配所有值。这样,类似“/api/bookings/2”这样的请求也可以匹配了。
那从业务逻辑的角度,可能书籍ID规定只是数字,因此通过通配符来匹配有些过于宽泛,希望能只匹配数字。Hoverfly也可以通过正则表达式来实现上述需求。相应的“path”匹配也可以修改为,
"path": [ {
"matcher": "exact",
"value": "/api/bookings/1" }],
"method": [ {
"matcher": "exact",
"value": "GET" }],
这样,Hoverfly只会匹配类似“/api/bookings/123”这样的请求,而类似“/api/bookings/a23”这样的请求则会被忽略。
模糊匹配和正则匹配还可以用于如日期、序号等接口请求中常见的场景,也通过这些匹配模式可以进一步提升Hoverfly在实际项目中的适用程度。
除了上述三种匹配方式之外,Hoverfly还支持XML和JSON格式的匹配,包括严格匹配以及部分匹配等逻辑。
模拟延迟
模拟接口的延迟也是接口测试中一个常见的场景。以下是Hoverfly的模拟文件中对某个指定接口实现固定的延迟。
{
"request": {
"path": [
{"matcher": "exact", "value": "/api/profile"}
],
"headers": {
"X-API-Version": [
{"matcher": "exact", "value": "v1"}
]
}
},
"response": {
"status": 404,
"body": "Page not found",
"fixedDelay": 3000
}}
除了固定延迟之外,还可以通过logNormalDelay 来指定随机延迟。除了指定某个接口之外,还可以指定全局的延迟。
无法录制的问题
在引入Hoverfly进行试点的初期,发现虽然示例用例可以跑通,但是在内部项目中无法对HTTP服务间调用进行录制。通过排查发现,内部项目使用了HttpClient来进行服务间调用,不过这个HttpClient来自于
HttpClient httpClient = HttpClients.createDefault()
而根据Hoverfly官网的说法,应该使用如下的方式来初始化HttpClient,
HttpClient httpClient = HttpClients.createSystem();
// orHttpClient httpClient = HttpClientBuilder.create().useSystemProperties().build();
回顾一下Hoverfly的工作原理,可以知道,Hoverfly-Java将启动Hoverfly服务,并作为HTTP Proxy,将HttpClient与服务端的往来通信拦截之后,进行流量的录制或者回放。
在Hoverfly-java中,有io.specto.hoverfly.junit.core.ProxyConfigurer类来负责相关的这些设置。
setProxySystemProperties() {
//...
keepOriginalProxyProperties(HTTP_NON_PROXY_HOSTS, HTTP_PROXY_HOST, HTTP_PROXY_PORT, HTTPS_PROXY_HOST, HTTPS_PROXY_PORT);
LOGGER.info("Setting proxy host to {}", hoverflyConfig.getHost());
System.setProperty(HTTP_PROXY_HOST, hoverflyConfig.getHost());
System.setProperty(HTTPS_PROXY_HOST, hoverflyConfig.getHost());
//...
LOGGER.info("Setting proxy proxyPort to {}", hoverflyConfig.getProxyPort());
System.setProperty(HTTP_PROXY_PORT, String.valueOf(hoverflyConfig.getProxyPort()));
System.setProperty(HTTPS_PROXY_PORT, String.valueOf(hoverflyConfig.getProxyPort()));}
可以看到,在这个setProxySystemProperties方法中,为JVM设置了HTTP_PROXY_HOST、HTTP_PROXY_PORT的系统变量。因此,如果使用了
HttpClientBuilder.create().useSystemProperties().build();
来获取HTTPClient实例,就可以将上述属性设置到HTTPClient之中,也就是将Hoverfly设置为HttpClient的代理服务器,从而发挥作用了。如果使用了
HttpClients.createDefault()
则不会携带上述系统变量,Hoverfly也就无从下手,发挥作用了。
因此,如果发现Hoverfly能顺利运行,但是没有录制成功,可以考虑排查一下服务间调用所使用的HTTP客户端是否携带了上述配置项。
增强:如何对录制结果进行修改
在实际的项目中,当服务间进行内部服务调用时,出于鉴权的需要,会在请求体中带上timeStamp,token等信息。这些信息经过录制之后会存放在指定的JSON文件之中。为了能够在用例执行时,可以让用例能够正确执行,需要手工将JSON文件中的匹配模式修改为glob,并将中的timeStamp,token的具体值修改为通配符*。如以下的案例,
"body": [ {
"matcher": "glob",
"value": "timeStamp=*,token=*" }],
由于手工修改较为繁琐,因此考虑通过客制化Hoverfly来实现这个目标。根据JUnit5的扩展机制,可以了解到Hoverfly-java-junit5是在HoverflyExtension中管理JSON文件导出的。
@Overridepublic void afterAll(ExtensionContext context) {
if (isRunning()) {
try {
verifyHoverflyValidate(context);
if (this.capturePath != null) {
this.hoverfly.exportSimulation(this.capturePath);
}
} finally {
this.hoverfly.close();
this.hoverfly = null;
}
}}
而Hoverfly的exportSimulation方法的具体定义如下,
/** * Exports a simulation and stores it on the filesystem at the given path
* * @param path the path on the filesystem to where the simulation should be stored
*/
public void exportSimulation(Path path) {
if (path == null) {
throw new IllegalArgumentException("Export path cannot be null.");
}
LOGGER.info("Exporting simulation data from Hoverfly");
try {
Files.deleteIfExists(path);
final Simulation simulation = hoverflyClient.getSimulation();
persistSimulation(path, simulation);
} catch (Exception e) {
LOGGER.error("Failed to export simulation data", e);
}}
其中的hoverflyClient.getSimulation就是负责从Hoverfly获取模拟的数据。因此可以有两个方案
1)修改已经生成的JSON文件
2)修改从hoverflyClient.getSimulation获取的Simulation数据,并保存成文件
由于Hoverfly-java并没有类似提供类似Middleware的接口来获取Simulation数据并进行修改,第二个方案较为复杂。因此可以考虑第一个方案,也就是
1)继承HoverflyExtension并复写afterAll方法,
2)首先根据现有方法来生成JSON文件,
3)然后根据capturePath来获取已生成的文件,并编写modify方法来修改并保存这个文件。
其余在项目感觉Hoverfly-java特别是JUnit5中需要的feature还可以有,
1)模拟数据聚合
考虑到对于某些请求可能有相同的应答,而某些用例的相同请求需要返回不同的结果。这种模式下可以考虑通过将不同内容的JSON文件通过父类类和子类的方式来实现复用和隔离,或者是通过类和方法上的注解来实现。不过目前来看,@HoverflySimulate注解只能在类上使用,不能注解在方法上,也不支持通过继承关系将两个或者多个@@HoverflySimulate注解提供的JSON文件的内容进行聚合来提供模拟数据。
2) 增量录制
@HoverflySimulate中的自动录制功能非常使用,但是该注解也约定,Hoverfly在发现请求响应文件后,只使用该文件进行匹配,而不是去向实际的对端微服务发送请求。如果在一个测试类中存放多个测试用例,在用例开发过程中,需要分开进行录制,最后进行请求/响应文件内容的合并。由于新用例所需的请求内容未匹配到,因此用例会执行失败。所以用例需要逐条开发并merge到最终的测试类中。由于Hoverfly-core包括中其实是支持增量录制的。
if (hoverfly.getHoverflyConfig().isIncrementalCapture()
&& capturePath != null
&& Files.isReadable(capturePath)) {
hoverfly.simulate(SimulationSource.file(capturePath));
}
因此,也希望Hoverfly团队能将该功能在JUnit5中实现。