写点什么

在 SpringBoot 微服务中模拟 gRPC

作者:Tom Akehurst

  • 2025-05-27
    北京
  • 本文字数:5747 字

    阅读完需:约 19 分钟

大小:1.77M时长:10:17
在SpringBoot微服务中模拟gRPC

当代码依赖大量外部服务时,端到端测试经常会因沙箱不可用、环境不稳定及测试数据设置难等问题变得缓慢且复杂。同时,运行大量针对沙箱 API 的自动化测试会使构建管道运行时间过长。


Mock 测试为解决上述问题提供了一种实用的替代方案,能够在全面测试和执行速度之间取得平衡。虽然市场上有许多工具能够支持 REST API 的 Mock,但随着 gRPC 的日益流行,针对其 Mock 支持的工具却相对匮乏,相关的讨论也相对较少。在本文中,我们将展示如何将熟悉的 JVM 工具——Spring Boot、gRPC 和 WireMock 结合在一起使用。我们还将讨论在测试模拟 gRPC API 时需要权衡的因素以及可能遭遇的潜在陷阱。

Mock、集成测试和权衡

API Mock 是一种权衡策略——以牺牲一些真实性为代价,换取更快、更确定且更易于构建的 API 实现。这种做法之所以有效,是因为许多测试类型(以及相应的风险管控)并不依赖于那些未在 Mock 中建模的 API 行为,因此我们可以在不损失关键反馈的情况下充分获得 Mock 带来的好处。


然而,有些缺陷只有在系统组件之间复杂的交互中才会暴露出来,而这些交互并未在 Mock 行为中被准确捕捉到。因此,在一个健全的测试策略中,仍然需要做一些集成测试。


好消息是,如果在创建测试时能够明确判断哪些测试旨在揭示上述复杂的集成问题,那么这些测试只需占整体测试套件的一小部分即可。

我们将使用的工具

首先,我们来明确一些即将用到的术语——如果你已经很熟悉这些概念,可以直接跳到下一部分。


gRPC 是一种基于 HTTP/2 并利用 Protocol Buffers(Protobuf)进行序列化的现代网络协议。它通常被用在对网络效率和低延迟要求较高的微服务架构中。


Spring Boot 是 Spring 生态系统的一个框架,遵循“约定优于配置”的原则,并提供智能的默认值,简化了 Java 应用程序开发。它让开发者能够专注于业务逻辑,同时在必要时仍然可灵活地进行自定义配置。


WireMock 是一款开源的 API 模拟工具,通过提供可配置的外部服务依赖模拟实现帮助开发者构建出可靠且确定性的测试环境。

为什么要在 Spring Boot 中模拟 gRPC?

在开发调用 gRPC 服务的应用程序或服务时,我们需要在开发过程中处理这些调用。以下是一些可供选择的方法:


  • 调用真实的沙箱服务。

  • 使用对象模拟工具(如Mockito)模拟客户端接口的定义。

  • 在本地模拟 gRPC 服务,并在开发和测试时将应用程序配置为连接到该服务。


第一个选项通常并不可行,原因如下:


  • 沙箱环境运行缓慢或不够稳定。

  • 沙箱中运行的服务版本与目标版本可能不一致。

  • 设置正确的测试数据存在困难。

  • 服务可能尚未开发完成。


第二个选项避免了上述问题,但由于并非所有与 gRPC 相关的代码都得到了执行,因此大大降低了测试的有效性。gRPC 是一种复杂的协议,存在许多潜在的故障模式,理想情况下,我们希望在自动化测试中发现这些故障模式,而不是等到暂存环境甚至更糟糕的是在生产环境中才发现。


因此,只剩下模拟 gRPC 服务这个选项,它既能执行 gRPC 集成代码,又能避免依赖外部沙箱所存在的种种问题。这正是我们在本文后续部分将重点探讨的内容。

难点所在

为测试设置模拟服务通常会引入配置复杂性,这可能会削弱模拟方法本身的好处。一个特别的痛点是使用固定端口号来运行模拟服务器,然后让本地环境去访问它们。使用固定端口会阻碍测试的并行化,随着测试套件数量的增长,扩展测试运行器会变得越来越困难。在某些共享租户的 CI/CD 环境中,固定端口还可能导致冲突和错误。


WireMock 与Spring Boot的集成通过动态端口分配和配置注入解决了这个问题。它在运行时自动分配可用端口,并将这些端口信息无缝注入到应用程序上下文中,从而免除了手动管理端口的繁琐工作,同时支持并行测试执行。这种功能与声明式注解配置的结合显著降低了设置复杂性,使测试基础设施更具可维护性。


第二个问题是,尽管 API 模拟工具能够很好地支持 REST 和 SOAP 协议,但对 gRPC 的支持却明显不足。为了弥补这一差距,我们将使用新发布的WireMock gRPC扩展。该扩展旨在为基于 gRPC 的架构带来与传统 HTTP 测试相同的模拟功能,同时保留开发者熟悉的存根模式。

放在一起

在下面的指南中,我们将构建一个 Spring Boot 应用程序,它调用一个简单的 gRPC 服务(“echo”),并返回请求中包含的消息。如果你想查看和运行示例代码,本文中的所有示例代码均来自此演示项目


我们将使用 Gradle 作为构建工具,但如有必要,可以很容易地将其改为 Maven。

第 1 步:创建一个空的 Spring Boot 应用程序

我们先从标准的 Spring Boot 应用程序结构入手。我们使用Spring Initializr来搭建我们的项目,并添加必要的依赖项:


plugins {    id 'org.springframework.boot' version '3.2.0'    id 'io.spring.dependency.management' version '1.1.4'    id 'java'}
group = 'org.wiremock.demo'version = '0.0.1-SNAPSHOT'
dependencies {    implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test'
复制代码

第 2 步:添加 gRPC 依赖项和构建任务

由于 gRPC 需要在运行时完全定义服务接口,因此我们需要在构建时执行以下操作:


  • 生成 Java 存根,这些存根将在设置 WireMock 中的模拟规则时使用。

  • 生成描述符(.dsc)文件,该文件将由 WireMock 在提供模拟响应时使用。


添加 gRPC 启动模块:


implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter:0.2.0'
复制代码


添加 Google Protobuf 库和 Gradle 插件:


plugins {   id "com.google.protobuf" version "0.9.4"}
复制代码


在依赖项部分:


protobuf 'com.google.protobuf:protobuf-java:3.18.1'protobuf 'io.grpc:protoc-gen-grpc-java:1.42.1'
复制代码


确保我们正在生成描述符和 Java 源代码:


protobuf {   protoc {      artifact = "com.google.protobuf:protoc:3.24.3"   }      plugins {      grpc {      }   }      generateProtoTasks {      all()*.plugins {         grpc {            outputSubDir = 'java'         }      }
      all().each { task ->         task.generateDescriptorSet = true task.descriptorSetOptions.includeImports = true          task.descriptorSetOptions.path = "$projectDir/src/test/resources/grpc/services.dsc"      }   }}
复制代码


添加 src/main/proto 文件夹,并在其中添加 Protobuf 服务描述文件:


package org.wiremock.grpc;
message EchoRequest {   string message = 1;}
message EchoResponse {   string message = 1;}
service EchoService {   rpc echo(EchoRequest) returns (EchoResponse);}
复制代码


添加上述内容后,我们就可以生成 Java 源代码和描述符:


./gradlew generateProto generateTestProto
复制代码


你现在应该可以在 src/test/resources/grpc 下看到一个叫作 services.dsc 的文件,以及在 build/generated/source/proto/main/java/org/wiremock/grpc 下有一些生成的源代码。

第 3 步:配置应用程序组件集成 gRPC

现在我们已经生成了 Java 存根,接下来可以编写一些使用它们的代码。


我们首先创建一个 REST 控制器,它接收对 /test-echo 的 GET 请求并调用 echo gRPC 服务:


@RestControllerpublic class MessageController {
   @Autowired   EchoServiceGrpc.EchoServiceBlockingStub echo;
   @GetMapping("/test-echo")   public String getEchoedMessage(@RequestParam String message) {       final EchoResponse response = echo.echo(EchoServiceOuterClass.EchoRequest.newBuilder().setMessage(message).build());       return response.getMessage();   }}
复制代码


接下来,我们对 Spring Boot 应用程序进行配置,初始化 gRPC 服务 Bean(这一步是为了能够将其注入到 REST 控制器中):


@SpringBootApplicationpublic class SpringbootGrpcApplication {
   @Bean   EchoServiceGrpc.EchoServiceBlockingStub echo(GrpcChannelFactory channels) {      return EchoServiceGrpc.newBlockingStub(channels.createChannel("local").build());   }    public static void main(String[] args) {      SpringApplication.run(SpringbootGrpcApplication.class, args);   }}
复制代码

第 4 步:设置集成测试类

现在,我们可以开始构建一些依赖 gRPC 模拟的测试。


首先,我们需要在测试类中配置一些东西:


@SpringBootTest(   webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,   classes = SpringbootGrpcApplication.class)@EnableWireMock({   @ConfigureWireMock(       name = "greeting-service",       portProperties = "greeting-service.port",       extensionFactories = { Jetty12GrpcExtensionFactory.class }   )})class SpringbootGrpcApplicationTests {
   @InjectWireMock("greeting-service") WireMockServer echoWireMockInstance; WireMockGrpcService mockEchoService;
   @LocalServerPort int serverPort; RestClient client; @BeforeEach void init() {       mockEchoService = new WireMockGrpcService(               new WireMock(echoWireMockInstance), EchoServiceGrpc.SERVICE_NAME       );       client = RestClient.create();   }}
复制代码


这里有很多东西:


  • @SpringBootTest 注解用于启动 Spring 应用程序,我们已将其配置为使用随机端口(我们最后要并行化测试套件,所以这正是我们需要的)。

  • @EnableWireMock 注解将 WireMock 集成添加到测试中,由嵌套的 @ConfigureWireMock 注解定义的单个实例。

  • 该实例配置为使用 Jetty12GrpcExtensionFactory ,即 gRPC WireMock 扩展。

  • @InjectWireMock 注解将 WireMockServer 实例注入到测试中。

  • 在 init() 方法中实例化的 WireMockGrpcService 是 gRPC 存根 DSL。它封装了 WireMock 实例,并指定了要模拟的 gRPC 服务的名称。

  • 我们将使用 RestClient 向 Spring 应用程序发出测试请求,使用 @LocalServerPort 指定的端口号。


现在我们准备编写测试。我们对 gRPC echo 服务做一些配置,让它返回一个简单的固定响应,然后向应用程序的 REST 接口发出请求,并验证消息是否如我们预期的那样传递:


@Testvoid returns_message_from_grpc_service() {   mockEchoService.stubFor(           method("echo")                   .willReturn(message(                           EchoResponse.newBuilder()                           .setMessage("Hi Tom")                   )));
   String url = "http://localhost:" + serverPort + "/test-echo?message=blah"; String result = client.get()           .uri(url) .retrieve() .body(String.class);    assertThat(result, is("Hi Tom"));}
复制代码


测试方法的第一行语句很有意思,基本上是在说“当调用 echo gRPC 方法时,返回一个带有消息值‘Hi Tom’的 EchoResponse”。


注意我们是如何利用由 protoc 工具(通过 Gradle 构建)生成的 Java 模型代码的。WireMock 的 gRPC DSL 采用了这些生成的模型和相关的构建器对象,这为我们提供了一种类型安全的方式来定义响应正文和预期的请求正文。


这样做的好处是,一方面,在构建存根代码中的正文对象时能够获得能够获得 IDE 的自动补全功能,另一方面,如果模型发生变化(由于.proto 文件发生变化),编译器会立即标记出来。

第 5 步:添加动态响应

在某些情况下,使用生成的模型类可能会显得过于局限,因此 WireMock 还支持将请求和响应正文作为 JSON 来处理。


例如,假设我们希望将请求中发送的消息回显到响应中,而不只是返回一个固定值。我们可以将 JSON 字符串作为响应正文进行模板化,其中 JSON 的结构与 protobuf 文件中定义的响应正文类型相匹配:


@Testvoid returns_dynamic_message_from_grpc_service() {   mockEchoService.stubFor(       method("echo").willReturn(           jsonTemplate(               "{\"message\":\"{{jsonPath request.body '$.message'}}\"}"           )));
   String url = "http://localhost:" + serverPort + "/test-echo?" +                "message=my-messsage";
   String result = client.get()           .uri(url)           .retrieve()           .body(String.class);
   assertThat(result, is("my-messsage"));}
复制代码


我们还可以利用 WireMock 提供的内置匹配器来处理请求的 JSON 表示:


mockEchoService.stubFor(   method("echo").withRequestMessage(           matchingJsonPath("$.message", equalTo("match-this"))       )       .willReturn(           message(EchoResponse.newBuilder().setMessage("matched!"))       ));
复制代码

当前的限制

目前,WireMock 的 gRPC 扩展对单向流的支持相当有限,只允许一个请求事件触发存根匹配,并且只允许在流式响应中返回一个事件。双向流方法目前根本不支持。


这些限制都是由于 WireMock 底层请求和响应模型的限制造成的,不过这些问题计划在即将发布的 4.x 核心库版本中得到解决。


当然,这些项目都是开源的,因此非常欢迎大家贡献自己的力量!


此外,目前只有有限范围的标准 Protobuf 功能经过了与扩展的测试,偶尔会发现一些不兼容性问题,对于这些问题,也非常欢迎大家提交问题和 PR。


大功告成!

如果你已经看到这里,希望你现在对如何使用 gRPC 模拟来支持 Spring Boot 集成测试有了一个清晰的概念。


请注意,虽然这是我们推荐的方法,但任何技术都存在权衡:模拟无法捕获所有真实世界的故障模式。我们建议使用契约测试或持续验证真实 API 等工具来提高测试的可靠性。


这里还有更多我们没有展示的内容,包括错误、故障、验证和热重载。如果想了解更多,gRPC扩展的文档测试代码是很好的学习资源。


有关 WireMock 与 Spring Boot 集成的更多信息,请参阅此页面


【声明:本文由 InfoQ 翻译,未经许可禁止转载。】


查看英文原文https://www.infoq.com/articles/mocking-grpc-microservices/

2025-05-27 17:001

评论

发布
暂无评论

健康讲座:如何提升人体免疫能力

石云升

学习 健康 7月日更

微信朋友圈高性能架构分析

面向对象的猫

架构实战营第二课作业——微信朋友圈的高性能复杂度分析

tt

架构实战营

编程的本质是什么?

白色蜗牛

Java 编程 程序员 软件 计算机

【数据结构】Java 同步工具 AQS

Alex🐒

Java 源码 数据结构

架构训练营模块二作业

老实人Honey

「架构师训练营第 1 期」

设计消息队列存储消息数据的MySQL表格

俞嘉彬

架构实战营

性能测试误差分析文字版-下

FunTester

软件测试 性能测试 接口测试 测试框架 测试开发

微信朋友圈架构设计

summer

极客时间 极客时间架构师一期

MVP on Board 没用小技巧 👌

newbe36524

.net MVP ASP.NET Core

央视曝光APP弹窗广告三大陷阱:如何监管应用软件弹窗广告

石头IT视角

《面试补习》--来聊聊削峰填谷!

九灵

Java 分布式 消息队列 异步削峰

大数据训练营-第一次作业

西伯利亚鼯鼠

2.4如何提高架构设计的质量

Lemon

架构实战营 - 模块二

Testcase

架构实战营

性能测试误差分析文字版-上

FunTester

性能测试 自动化测试 接口测试 测试框架 测试开发

Presto原理&调优&面试&实战全面升级版

王知无

架构训练营 1 期 - 模块二作业

蔸蔸

区块链的宿命,数字经济的局

CECBC

架构实战营 - 模块二(作业)

Cingk

智能运维系列之五:总结

micklongen

AIOPS 智能运维

面试算法之螺旋数组查找问题

泽睿

面试 二分查找

进阶指南!深入理解Java注解

Jackpop

Java

架构实战营 - 模块 2 - 微信朋友圈高性能复杂度分析

雪中亮

架构实战营 #架构实战营

模块二作业

俊杰

2.3如何设计高可用架构

Lemon

存储高可用

知乎热文 | 如何高效学习Spring Boot?

Jackpop

Java Spring Boot

清晰了!一文彻底理解Java事件处理

Jackpop

Java

架构训练营第 1 期 模块二作业

高远

架构训练营模块二作业

BlingBling

架构实战营

在SpringBoot微服务中模拟gRPC_后端_InfoQ精选文章