hoverfly
重要要点
- 在微服务体系结构中,服务最重要的部分之一是负责与其他服务通信的模块。
- 您通常需要端到端测试服务与其他服务的通信方式。 模拟不是解决方案,因为它不测试通信堆栈,并且跳过与网络协议(即HTTP)有关的所有内容。 由于每次都要准备该过程,因此无法选择运行依赖服务。
- 服务虚拟化是一种用于通过创建代理服务来模拟服务依赖关系行为的技术,因此测试针对服务运行(即测试了整个堆栈),而无需启动真实服务。
- Hoverfly是一种使用Go编程语言编写并与Java紧密集成的开源,轻量级,服务虚拟化API仿真工具。
在微服务体系结构中,该应用程序由多个互连的服务组成,其中所有这些协同工作可产生所需的业务功能。 因此,典型的企业微服务架构如下所示:
每个服务都依赖于其他服务,其中一些由数据库支持。 每个服务必须具有自己的一组测试(单元,组件,合同等),以验证其正确性,例如在更改之后。
让我们专注于图中间的一项服务; 如果您向里看,您会看到与下图非常相似的内容:
服务通常包含这些层中的一些(如果不是全部的话),概括而言,可以描述如下:
- 资源:充当服务的入口点。 它们解组以任何协议(即JSON)形式出现的消息,并转换为域对象。 同样,它们验证输入参数是否有效,并负责将域对象转换为协议消息以返回响应的封送处理。 例如,如果您的技术是Jakarta EE / MicroProfile,则JAX-RS会处理所有这些方面,而Spring Boot或任何其他技术也应如此。
- 业务:业务逻辑是实现服务的业务规则并编排服务的所有部分(例如持久性和网关层)的业务逻辑。 域对象是此层的一部分。
- 持久性:这是将域对象保存到“持久”后端(SQL或NoSQL数据库)的部分。 如果您的技术是Jakarta EE / MicroProfile,那么这是处理JPA的部分(对于SQL数据库)。
- 网关:到目前为止,所有其他部分在所有其他体系结构中都是通用的,但是网关是分布式体系结构的典型层。 网关封装了所有逻辑,以便从消费者服务(实现网关的地方)到提供者服务(基础协议和域对象之间的编组/解组操作)和网络客户端的配置,超时,重试,弹性等进行通信。您使用Jakarta EE / MicroProfile和JSON作为消息传递协议,则可能使用JAX-RS客户端作为Http客户端与另一服务进行通信。
您可以通过模拟网关层来测试顶层。 例如,使用Mockito来测试业务层的代码可能类似于:
@Mock
WorldClockServiceGateway worldClockServiceGateway;
@Test
public void should_deny_access_if_not_working_hour() {
// Given
when(worldClockServiceGateway.getTime("cet")).thenReturn(LocalTime.of(0,0));
SecurityResource securityResource = new SecurityResource();
securityResource.worldClockServiceGateway = worldClockServiceGateway;
// When
boolean access = securityResource.isAccessAllowed();
// Then
assertThat(access).isFalse();
}
但是当然,您仍然需要验证Gateway类(WorldClockServiceGateway)是否按预期工作:
- 它能够根据特定于域对象的协议进行封送/取消封送
- (Http)客户端配置参数正确(例如,超时,重试,标头等)
- 在发生网络错误时,它的行为符合预期
为了测试所有这些点,您可能会考虑运行网关与之通信的服务并针对真实服务进行测试。 这似乎是一个不错的解决方案,但存在一些问题:
- 您需要知道如何从使用者服务以及提供者所依赖的所有传递服务以及所需的数据库中启动提供者服务。 这看起来像是对单一职责的违反,消费者服务应该只知道如何部署自身,而不是他们的依赖服务。
- 如果任何服务都需要数据库,则必须准备数据集。
- 启动多个服务还意味着它们中的任何一个都可能由于内部/网络错误而失败,因此使测试失败不是由于网关类错误,而是由于基础结构,使得该测试不稳定。
- 此外,启动所有必需的服务(即使只有一项)也可能花费大量时间,因此您的测试套件无法提供快速反馈。
您可能会想到的解决方案之一就是跳过这种测试,因为它们通常很不稳定,并且执行启动整个Universe来运行简单的网关测试需要花费大量时间。 但是微服务体系结构中的通信部分是核心部分,正是该部分发生了与系统的任何交互,因此进行测试以验证其行为符合预期非常重要。
解决此问题的方法是服务虚拟化。
什么是服务虚拟化?
服务虚拟化是一种用于模拟服务依赖项行为的技术。 尽管服务虚拟化通常与基于REST API的服务相关联,但是相同的概念也可以应用于任何其他类型的依赖项,例如数据库,ESB,JMS等。
除了帮助内部服务测试外,服务虚拟化还可以帮助您测试不受您控制的服务,从而解决了使此类测试变得不稳定的一些常见问题。 他们之中有一些是:
- 您的网络已关闭,因此您无法与外部服务进行通信。
- 外部服务已关闭,并且出现一些意外错误。
- API的限制。 一些公共API在费率/天上有一些限制。 如果达到此级别,则测试将开始失败。
使用服务虚拟化,您可以避免所有这些问题,因为您不是在提供真正的服务,而是虚拟的服务。
但是服务虚拟化不仅仅可以用于测试幸福的路径情况,但是许多开发人员和测试人员发现,它的真正力量在于难以对真实服务进行测试的边缘情况,例如在低延迟响应下服务的行为或出现意外错误。
如果考虑如何测试整体架构中的组件,那么您一直在对象之间使用类似的东西,这称为模拟。 使用模拟时,您通过提供方法调用的固定答案来模拟对象的行为。 使用服务虚拟化时,您正在执行类似的操作,但不是在模拟对象的行为,而是在提供远程服务的固定答案。 因此,服务虚拟化有时被称为企业模拟。
在下图中,您可以看到服务虚拟化如何工作:
在这种具体情况下,服务之间的通信是通过HTTP协议进行的,因此,使用瘦HTTP服务器负责消费来自网关类的请求并提供固定的答案。
运行模式
一般来说,服务虚拟化有两种模式:
- 回放模式:使用模拟数据以提供响应,而不是将其转发到实际服务。 可以手动创建模拟数据(如果尚不存在实际服务)或使用捕获模式创建。
- 记录模式:拦截服务之间的通信,并记录来自真实服务的传出请求和传入响应。 通常,捕获模式被用作创建初始模拟数据的过程的起点。
根据实现的不同,它们可能包含其他模块,但是所有模块都应包含这两种模式。
气垫蝇
什么是Hoverfly?
Hoverfly是一种使用Go编程语言编写的开源,轻量级,服务虚拟化API仿真工具。 它还提供了与Java紧密集成的语言绑定。
Hoverfly Java
Hoverfly Java是Hoverfly的Java包装器,使您摆脱了Hoverfly的安装并管理其生命周期。 Hoverfly Java提供了Java DSL以编程方式生成模拟数据,并与JUnit和JUnit5进行了深度集成。
Hoverfly Java将网络Java系统属性设置为使用Hoverfly代理。 这实际上意味着Hoverfly代理将拦截Java运行时与物理网络层之间的所有通信。 这意味着,尽管您的HTTP Java客户端可能指向诸如http://worldclockapi.com之类的外部站点,但该连接将被Hoverfly拦截并转发。
重要的是要注意,如果您的Http客户端不接受Java网络代理设置,则需要手动进行设置。
至此,我们将交替使用Hoverfly和Hoverfly Java来指代Hoverfly Java。
为了将Hoverfly添加到JUnit 5中,您需要在构建工具上注册下一个依赖项:
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java-junit5</artifactId>
<version>0.11.5</version>
<scope>test</scope>
</dependency>
Hoverfly模式
除了“播放”(在Hoverfly中称为“模拟”)和“记录”(在Hoverfly中称为“捕获”)模式之外,Hoverfly还实现了其他模式:
- 间谍:如果在模拟数据中找到请求匹配项,则模拟外部API,否则,将请求转发到真实API。
- 合成:将请求直接传递到中间件(已定义的可执行文件),而不是在模拟数据中寻找响应,该中间件负责使用和生成所需的响应。
- 修改:将请求发送到中间件,然后再将其转发到目的地。 响应也将在返回给客户端之前传递到可执行文件。
- 差异:将请求转发到外部服务,并将响应与当前存储的仿真进行比较。 通过存储的模拟响应和外部服务的实际响应,Hoverfly能够检测到两者之间的差异。 这些差异存储起来,以备将来使用。
Hoverfly示例
为了演示Hoverfly的工作原理,我们假设我们有一个服务A(安全服务),该服务需要了解http://worldclockapi.com上部署的另一个服务提供的当前时间。 当您对http://worldclockapi.com/api/json/cet/执行GET请求时,现在返回下一个JSON文件:
{
"$id":"1",
"currentDateTime":"2019-03-12T08:10+01:00",
"utcOffset":"01:00:00",
"isDayLightSavingsTime":false,
"dayOfTheWeek":"Tuesday",
"timeZoneName":"Central Europe Standard Time",
"currentFileTime":131968518527863732,
"ordinalDate":"2019-71",
"serviceResponse":null
}
文档中的重要字段是currentFileTime,它提供了全时信息。
负责与服务进行通信并返回当前时间的网关类如下所示:
public class ExternalWorldClockServiceGateway implements WorldClockServiceGateway {
private OkHttpClient client;
public ExternalWorldClockServiceGateway() {
this.client = new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
@Override
public LocalTime getTime(String timezone) {
final Request request = new Request.Builder()
.url("http://worldclockapi.com/api/json/"+ timezone + "/now")
.build();
try (Response response = client.newCall(request).execute()) {
final String content = response.body().string();
final JsonObject worldTimeObject = Json.parse(content).asObject();
final String currentTime = worldTimeObject.get("currentDateTime").asString();
final DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
LocalDateTime localDateTime = LocalDateTime.parse(currentTime, formatter);
return localDateTime.toLocalTime();
} catch(IOException e) {
throw new IllegalStateException(e);
}
}
}
这里的重要部分是URL不是参数。 我知道在一个真实的示例中,此信息将来自配置参数,但是为了简单起见,并且因为您可以发现Hoverfly正在代理所有网络通信,所以该URL是硬编码的。
让我们开始看看一些可能的情况以及如何测试此ExternalWorldClockGateway类。
世界时钟服务尚未开发
如果尚未开发WorldClockService,我们需要在仿真模式下使用Hoverfly并提供罐头答案。
@ExtendWith(HoverflyExtension.class)
public class ExternalWorldClockServiceGatewayTest {
private static final String OUTPUT = "{\n"
+ " \"$id\":\"1\",\n"
+ " \"currentDateTime\":\"2019-03-12T10:54+01:00\",\n"
+ " \"utcOffset\":\"01:00:00\",\n"
+ " \"isDayLightSavingsTime\":false,\n"
+ " \"dayOfTheWeek\":\"Tuesday\",\n"
+ " \"timeZoneName\":\"Central Europe Standard Time\",\n"
+ " \"currentFileTime\":131968616698822965,\n"
+ " \"ordinalDate\":\"2019-71\",\n"
+ " \"serviceResponse\":null\n"
+ "}";
@Test
public void should_get_time_from_external_service(Hoverfly hoverfly) {
// Given
hoverfly.simulate(
SimulationSource.dsl(
HoverflyDsl.service("http://worldclockapi.com")
.get("/api/json/cet/now")
.willReturn(success(OUTPUT, "application/json"))
)
);
final WorldClockServiceGateway worldClockServiceGateway = new ExternalWorldClockServiceGateway();
// When
LocalTime time = worldClockServiceGateway.getTime("cet");
// Then
Assertions.assertThat(time.getHour()).isEqualTo(10);
Assertions.assertThat(time.getMinute()).isEqualTo(54);
}
}
重要的部分是模拟方法。 此方法用于导入测试与远程Hoverfly代理之间交互的模拟。 在此具体示例中 ,它将Hoverfly代理配置为在收到端点的GET请求时返回固定答案,而不是将流量转发到本地主机之外。
世界时钟服务开发并运行
如果服务已经在运行,则可以使用捕获模式来生成初始模拟数据集,而不必手动生成它们。
@ExtendWith(HoverflyExtension.class)
@HoverflyCapture(path = "target/hoverfly", filename = "simulation.json")
public class ExternalWorldClockServiceGatewayTest {
@Test
public void should_get_time_from_external_service() {
// Given
final WorldClockServiceGateway worldClockServiceGateway = new ExternalWorldClockServiceGateway();
// When
LocalTime time = worldClockServiceGateway.getTime("cet");
// Then
Assertions.assertThat(time).isNotNull();
在此测试案例中,Hoverfly以捕获模式启动。 这意味着请求通过真实服务发生,并且请求和响应被本地存储以在模拟模式下重用。 在先前的测试中,模拟数据放置在target / hoverfly目录中。
一旦存储了模拟数据,您就可以切换到模拟模式,因此不再与真实服务进行任何通信。
@ExtendWith(HoverflyExtension.class)
@HoverflySimulate(source =
@HoverflySimulate.Source(value = "target/hoverfly/simulation.json",
type = HoverflySimulate.SourceType.FILE))
public class ExternalWorldClockServiceGatewayTest {
@Test
public void should_get_time_from_external_service() {
// Given
final WorldClockServiceGateway worldClockServiceGateway = new ExternalWorldClockServiceGateway();
// When
LocalTime time = worldClockServiceGateway.getTime("cet");
// Then
Assertions.assertThat(time).isNotNull();
HoverflySimulate批注允许您从文件,类路径或URL导入模拟。
您可以设置HoverflyExtension以自动在模拟和捕获模式之间切换。 如果找不到源,它将在捕获模式下运行,否则,将使用模拟模式。 这意味着您无需手动切换使用@HoverflyCapture和@HoverflySimulate 。 该Hoverfly功能非常简单,但功能非常强大。
@HoverflySimulate(source =
@HoverflySimulate.Source(value = "target/hoverfly/simulation.json",
type = HoverflySimulate.SourceType.FILE),
enableAutoCapture=true)
检测过时的模拟数据
使用服务虚拟化时,您面临的问题之一是模拟数据过时会发生什么,因此,尽管所有测试都是绿色的,但针对真实服务运行代码时可能会失败。
为了检测到此问题,Hoverfly实现了Diff模式,该模式将请求转发到远程服务,并将响应与存储的数据模拟进行比较。 当Hoverfly完成对两个响应的比较后,将存储差异,并为来自远程服务的真实响应提供输入请求。 之后,使用Hoverfly Java,您可以断言没有发现任何差异。
@HoverflyDiff(
source = @HoverflySimulate.Source(value = "target/hoverfly/simulation.json",
type = HoverflySimulate.SourceType.CLASSPATH))
通常,您不想一直运行Diff模式。 在实践中,这将取决于许多因素,例如,是否控制远程服务,或者是否已大量开发远程服务。 根据具体情况,您将希望每天,每周一次或计划进行最终发行时以Diff模式运行测试。
验证任务的典型工作流程是:
- 运行差异模式测试。
- 如果发生故障,则删除过时的模拟数据。
- 触发新的服务构建作业,以强制Hoverfly重新捕获模拟。
- 由于新捕获的数据与先前捕获的数据不同,因此构建可能会失败。 然后,开发人员会看到该服务失败,可以开始修复代码以适应新的服务输出。
延迟回应
Hoverfly允许您在发送回响应之前添加一些延迟。 这使您可以模拟延迟并测试服务是否正确处理了延迟。 要配置它,您只需要使用Simulation DSL来设置要应用的延迟。
hoverfly.simulate(
SimulationSource.dsl(
HoverflyDsl.service("http://worldclockapi.com")
.get("/api/json/cet/now")
.willReturn(success(OUTPUT, "application/json")
.withDelay(1, TimeUnit.MINUTES)
)
));
验证中
我已经提到过,您可以将服务虚拟化视为针对企业的模拟。 模拟的最常用功能之一是,您可以验证在测试阶段是否已调用了具体方法。
使用Hoverfly,您可以执行完全相同的操作,以验证是否已对远程服务端点发出了特定请求。
hoverfly.verify(
HoverflyDsl.service("http://worldclockapi.com")
.get("/api/json/cet/now"), HoverflyVerifications.times(1));
上一个代码片段验证网关类是否已从主机worldclockapi.com到达端点/ api / json / cet / now。
更多功能
Hoverfly还实现了此处未显示但在某些情况下有用的其他功能。
- 请求字段匹配器:默认情况下,当您传递字符串时,DSL请求构建器将假定完全匹配。 您还可以通过匹配器,以使匹配不严格(即service(matches(“ www。*-test.com ”)))
- 响应模板:当您需要基于请求数据动态构建响应时,可以使用模板来进行。
- SSL:当请求通过Hoverfly时,它需要解密它们才能持久(捕获模式)或执行匹配。 因此,您最终在Hoverfly和远程服务之间使用SSL,然后在客户端和Hoverfly之间再次使用SSL。 为了使此过程顺利进行,Hoverfly附带了自己的自签名证书,在实例化该证书时会自动信任该证书。
- 有状态模拟:有时您需要根据先前的请求在响应中添加一些状态。 例如,在发送删除请求之后,如果查询已删除的元素,您可能会收到404错误状态错误。
SimulationSource.dsl(
service("www.example-booking-service.com")
.get("/api/bookings/1")
.willReturn(success("{\"bookingId\":\"1\"}", "application/json"))
.delete("/api/bookings/1")
.willReturn(success().andSetState("Booking", "Deleted"))
.get("/api/bookings/1")
.withState("Booking", "Deleted")
.willReturn(notFound())
在前面的代码中,当使用DELETE HTTP方法将请求发送到/ api / bookings / 1时,Hoverfly会将状态设置为Deleted。 当使用GET HTTP方法将请求发送到/ api / bookings / 1时,由于状态为Deleted,因此未找到错误返回。
合同测试
服务虚拟化是另一种可以帮助您编写测试的工具,但是它不能替代合同测试。
合同测试的主要目的是验证从业务的角度来看,消费者和提供者的服务都将能够正确通信,并且双方都遵守了他们同意遵守的合同。
另一方面,服务虚拟化可用于:
- 测试已经创建但尚未签订任何合同的服务。
- 测试与我们可能没有合同的第三方服务的集成。
- 测试极端情况(延迟,无效输入等)
- 协助您编写集成测试,其中客户端比从属服务的更改频率更高。
- 在不依赖服务的情况下进行测试,或者进行代表性的负载测试很昂贵。
- 加速服务可以最大程度地减少本地笔记本电脑上的内存占用。
- 测试(及其数据)在合同测试中很难创建或维护。
结论
服务虚拟化(和Hoverfly)是可以用来测试服务(尤其是服务之间的通信)的另一种工具,您可以使用一种基于“单元”测试的方法来进行测试。 我之所以说“单元”,是因为您的测试实际上并未运行远程服务,因此您仅测试系统的一个单元。 这使您无需运行整个系统即可测试通信层,并且无需初始化整个堆栈就可以进行测试。 注意,从接受测试的消费者服务的角度来看,它对提供者服务一无所知,因此虚拟服务与真实服务没有什么不同。 反过来,这意味着消费者服务不知道它是否生活在模拟中。
随着微服务体系结构的到来,服务虚拟化是一种必不可少的工具,可以避免与其他服务进行通信时出现意外情况,尤其是在具有大量依赖关系的企业环境中工作时。 服务虚拟化还可用于在测试阶段消除对第三方服务的依赖性,并用于测试应用程序遇到延迟或其他网络问题时的行为。 最后,在具有要求访问第三方服务的单片应用程序的情况下,服务虚拟化也可以在遗留项目中使用,否则很难进行仿真。
关于作者
是Red Hat开发人员小组的软件工程师。 他对Java世界和软件自动化充满热情,并相信开源软件模型。 Alex是NoSQLUnit和Diferencia项目的创建者,JSR374(用于JSON处理的Java API)专家组的成员,Manning撰写的Testing Java Microservices一书的合著者,并且是多个开源项目的撰稿人。 自2017年以来一直是Java冠军和国际演讲者,他谈到了微服务的新测试技术以及21世纪的持续交付。 您可以在Twitter @alexsotob上找到他。
hoverfly
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/34078.html