返回

Spock 迁移 Quarkus 测试:JerseyTest 替换 @QuarkusTest 详解

java

用 @QuarkusTest 替换 Spock 规范中的 JerseyTest:可行吗?怎么做?

手里有一堆用 Spock 写的 JAX-RS 资源“单元测试”,原本是跑在一个 Open Liberty 项目里的。像下面这样:

class ExampleResourceSpec extends ResourceSpecification {

    static final String BASE_URL = '/myurl'

    @Shared
    private ExampleService service

    @Override
    protected createResource() {
        service = Mock() // 使用 Spock 的 Mock
        new ExampleResource(service)
    }

    def '请求成功时返回 200 状态码'() {
        given: '一个有效的请求'
        MyRequest request = fixtures.createMyRequest()
        def jsonReq = createJsonMyRequest(request) // 转换为 JSON 字符串

        when: '发起 POST 请求'
        // 关键点:之前用 JerseyTest 发起请求
        def response = jerseyPost(jsonReq, BASE_URL)

        then: 'service 被正确调用'
        1 * service.handle({ actualRequest -> // 验证 Mock 交互
            actualRequest.field1 == request.field1 &&
            actualRequest.field2 == request.field2 &&
            actualRequest.field3 == request.field3
        })

        and: '资源返回 200 状态码'
        response.status == Response.Status.OK.statusCode
    }

    // 使用 JerseyTest 发起 POST 请求的辅助方法
    private Response jerseyPost(String jsonReq, String url) {
        jerseyTest
                .target(url)
                .request()
                .post(Entity.json(jsonReq))
    }

    // 创建 JSON 请求体的辅助方法
    private createJsonMyRequest(MyRequest request) {
        def jsonSlurper = new JsonSlurper()
        def parsedJson = jsonSlurper.parseText(jsonb.toJson(request))
        return new JsonBuilder(parsedJson).toString()
    }
}

这些测试都继承了一个基类 ResourceSpecification,里面封装了 JerseyTest 的启动和关闭逻辑,它会启动一个 Grizzly 服务器来运行 JAX-RS 资源:

import spock.lang.Specification
import javax.ws.rs.core.Application
import javax.ws.rs.core.Response
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.test.JerseyTest
import javax.json.bind.JsonbBuilder

abstract class ResourceSpecification extends Specification {

    // 持有 JerseyTest 实例
    JerseyTest jerseyTest
    def jsonb = JsonbBuilder.create() // 用于 JSON 操作

    // 抽象方法,由子类实现来创建具体的 JAX-RS 资源实例
    protected abstract createResource();

    def setup() {
        // 初始化 JerseyTest
        jerseyTest = new JerseyTest() {
            @Override
            protected Application configure() {
                // 配置 JAX-RS 应用,注册资源和异常映射器
                new ResourceConfig()
                        .register(createResource()) // 注册子类创建的资源
                        .register(MyAwesomeExceptionMapper) // 注册通用异常处理器
            }
        }
        // 启动 JerseyTest 管理的服务器
        jerseyTest.setUp()
    }

    // 解析响应体为 Map 的辅助方法
    def parse(Response response) {
        jsonb.fromJson(response.readEntity(String), Map.class)
    }

    def cleanup() {
        // 关闭 JerseyTest 管理的服务器
        jerseyTest.tearDown()
    }
}

现在项目要迁移到 Quarkus。麻烦来了,Quarkus 对 Spock 的支持并不完美,特别是涉及到 CDI (ArC) 集成。而且,我们想扔掉 JerseyTest 和它背后的 Grizzly 服务器,改用 Quarkus 自带的 @QuarkusTest 注解来跑测试。这个注解能启动一个真实的 Quarkus 应用环境。

问题是,测试代码量很大,如果全部用 JUnit 5 重写,工作量实在有点吓人。

尝试过的路子

  1. REST Assured + 手动启动服务器: 试过用 REST Assured 替换 JerseyTest 来发 HTTP 请求,同时用 GrizzlyHttpServerFactory 手动搭了个服务器跑资源。效果不错,测试能跑通。但这违背了初衷——我们想用 Quarkus 的能力,而不是自己搞一套服务器启动逻辑,还引入了额外的 Grizzly 依赖。

  2. REST Assured + @QuarkusTest (失败): 把手动启动服务器的代码干掉,在 ResourceSpecification 类上加了 @QuarkusTest 注解,期望它能启动 Quarkus 服务器。结果,REST Assured 发请求时直接报 ConnectionException,连不上服务器。

感觉问题出在 Spock 和 Quarkus 的 CDI 容器 ArC 之间缺乏像 JUnit 5 那样的“官方”集成。

问题分析:为什么直接替换行不通?

@QuarkusTestJerseyTest 的工作方式有本质区别。

  • JerseyTest 它主要关注 JAX-RS 层面。它启动一个轻量级服务器(默认 Grizzly),加载你明确指定的 JAX-RS Resource 类和 Application 配置。它不关心完整的应用上下文,比如 CDI 容器、配置注入等,除非你在 ResourceConfig 里手动配置。在你的例子里,createResource() 返回的资源实例,以及其依赖 service (被 Spock Mock 了),都是在测试代码里手动创建和管理的,和 CDI 关系不大。

  • @QuarkusTest 这个注解启动的是一个几乎完整的 Quarkus 应用。它会初始化 CDI 容器 (ArC),扫描和管理 Bean,处理 @Inject,加载 application.properties 配置等等。它的目标是提供一个尽可能接近生产环境的测试运行时。

当你尝试在 Spock 测试类上用 @QuarkusTest 时,会遇到几个核心障碍:

  1. 生命周期不匹配: Spock 的测试实例生命周期管理和 JUnit 5 (Quarkus 主要支持的) 不一样。@QuarkusTest 的底层机制是为 JUnit 5 设计的,用来在测试前后启动和关闭 Quarkus 应用。它可能无法正确地与 Spock 的 setup()cleanup() (或者 @BeforeAll/@AfterAll 对应的 setupSpec()/cleanupSpec()) 方法协调工作。

  2. CDI 集成缺失: 这是关键。Quarkus 的测试框架与 ArC 深度集成。在 JUnit 5 测试中,你可以用 @Inject 来注入 Quarkus 管理的 Bean,用 @InjectMock 来轻松替换某个 Bean 为 Mockito mock 对象。Spock 测试默认情况下享受不到这些便利。即使 @QuarkusTest 成功启动了服务器,Spock 测试本身也无法直接利用 Quarkus 的 CDI 功能来获取或模拟 Bean。你那个手动创建 ExampleResource 实例并传入 Spock Mock service 的做法,绕过了 Quarkus 的 CDI 容器。当 @QuarkusTest 启动时,Quarkus 容器可能也尝试根据自己的规则去发现和创建 ExampleResource(如果它也被注解为 CDI Bean 的话),这可能导致冲突或行为不符合预期。REST Assured 连不上服务器,很可能是因为 Quarkus 应用没能按预期完全启动,或者启动的 JAX-RS 端点配置跟你手动创建资源时期望的不一致。

  3. 依赖注入问题: Spock 的 @SharedMock() 是 Spock 自己的机制。它们创建的对象对于 Quarkus 的 ArC 容器来说是“外部”的。当你的 ExampleResource 作为一个 Quarkus Bean 被容器管理时,它期望的依赖(比如 ExampleService)会由 ArC 来注入。这时,Spock 的 Mock 对象就插不进去了,除非使用 Quarkus 提供的 @InjectMock (但这又回到了需要 JUnit 5 集成的问题)。

解决方案探讨

摆在面前的路似乎有两条,各有优劣。

方案一:坚持使用 Spock(有代价)

这条路比较坎坷,因为官方集成度不高。你需要接受一些限制,或者进行更复杂的配置。

  1. 目标:@QuarkusTest 启动 Quarkus 应用,用 REST Assured 发请求,但 Mocking 机制可能需要调整。

  2. 原理: 尝试利用 @QuarkusTest 把 Quarkus 应用跑起来,这样 JAX-RS 端点就能用了。然后用 REST Assured (一个纯 HTTP 客户端) 去访问这些端点。核心挑战在于如何处理依赖 Mock。

  3. 操作步骤与代码示例:

    • 添加依赖: 确保 pom.xmlbuild.gradle 中有 quarkus-spock (如果存在且有帮助的话,但通常它解决不了 CDI 集成问题) 和 quarkus-resteasy-jackson (或对应的 JSON 库) 以及 rest-assured

      <!-- pom.xml 示例 -->
      <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-spock</artifactId>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>io.rest-assured</groupId>
          <artifactId>rest-assured</artifactId>
          <scope>test</scope>
      </dependency>
      <!-- 根据你的 JAX-RS 实现和 JSON 库添加,例如 -->
      <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy</artifactId> <!-- 或 quarkus-resteasy-reactive -->
      </dependency>
      <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy-jackson</artifactId> <!-- 或 -jsonb -->
      </dependency>
      
    • 修改测试基类 ResourceSpecification

      • 移除 JerseyTest 相关的所有代码 (jerseyTest 字段, setup(), cleanup() 里关于 jerseyTest 的部分)。
      • 在类上添加 @QuarkusTest
      • 不再需要 createResource() 方法,因为 Quarkus 会自动扫描并部署 JAX-RS 资源(假设 ExampleResource 是一个符合 JAX-RS 和 CDI 规范的 Bean,比如带有 @Path@ApplicationScoped / @RequestScoped 等)。
      • 你需要配置 REST Assured 的基础 URI 和端口,通常 @QuarkusTest 会用 8081 端口。
    • 修改测试类 ExampleResourceSpec

      • 移除 @Override createResource()
      • REST Assured 替换 jerseyPost 方法。
      • 处理 Mocking (难点): 这是最棘手的部分。由于 Spock 和 ArC 集成不佳:
        • 无法使用 @InjectMock: Quarkus 提供的 @InjectMock 是为 JUnit 5 设计的,用于替换 CDI Bean。在 Spock 里直接用基本行不通。

        • Spock 的 Mock() 可能无效: 你之前在 createResource() 里创建的 Mock service 是注入到手动创建的 ExampleResource 实例中的。现在 ExampleResource 由 Quarkus 管理,它会尝试注入一个真实的(或配置的)ExampleService Bean。Spock 的 Mock 对象无法自动替换掉这个 Bean。

        • 潜在的 workaround (复杂且不推荐):

          • 全局 Mock 替换 (需要 @Alternative@Priority): 你可以创建一个 ExampleService 的 Mock 实现,并将其声明为 CDI 的 Alternative,并给予高优先级,使其在测试时覆盖掉真实的 Bean。这需要在 src/test/java (或 groovy) 下创建 Mock 类,并修改 application.properties 或使用 @TestProfile。这种方式比较重,而且是全局性的,可能影响其他测试。

          • 使用 QuarkusMock (编程方式 Mock): Quarkus 提供了一个 QuarkusMock 工具类,可以在测试方法内部(通常是 @BeforeEach / setup())以编程方式安装 Mock。你需要手动处理 Mock 的生命周期。这与 Spock 的声明式 Mock 风格不太搭。

            // 在 setup() 或测试方法内部
            def mockedService = Mock(ExampleService)
            QuarkusMock.installMockForInstance(mockedService, getInstance(ExampleService.class))
            // getInstance 需要辅助方法来从 CDI 容器获取 Bean 实例
            
            // ... 在 cleanup() 或测试方法后需要 uninstall
            // QuarkusMock.uninstallMock(getInstance(ExampleService.class))
            

            获取 Bean 实例可能也需要用到 CDI.current().select(ExampleService.class).get() 这样的方式,进一步增加了复杂性。

        • 放弃 Mock,进行集成测试: 如果 Mock 非常困难,可以考虑将这类测试视为更偏向集成测试,测试 ExampleResource 和它依赖的真实 ExampleService 一起工作的情况。这要求 ExampleService 的真实实现在测试环境中也能运行(比如连接到内存数据库 H2)。

    • 示例 (使用 REST Assured 替换 HTTP 调用):

      // 在 ExampleResourceSpec 中
      import static io.restassured.RestAssured.given
      import io.restassured.http.ContentType
      
      // ... 其他部分 ...
      
      def '请求成功时返回 200 状态码'() {
          given: '一个有效的请求'
          MyRequest request = fixtures.createMyRequest()
          // JSON 转换保持不变或使用 REST Assured 的能力
          // def jsonReq = createJsonMyRequest(request)
      
          when: '发起 POST 请求'
          // 使用 REST Assured
          def response = given()
              .contentType(ContentType.JSON)
              .body(request) // REST Assured 可以直接接受对象并序列化
          .when()
              .post(BASE_URL) // BASE_URL '/myurl'
          .then()
              .extract().response() // 获取 RestAssured Response
      
          then: 'service 被正确调用 (这里假设 Mocking 问题已解决,例如通过 Alternative)'
          // 验证 Mock 交互的代码需要根据 Mocking 实现方式调整
          // 如果无法 Mock,这部分验证就需要去掉或修改
      
          and: '资源返回 200 状态码'
          response.statusCode() == 200 // Response.Status.OK.statusCode
      }
      
      // 不再需要 jerseyPost 和 createJsonMyRequest (如果 REST Assured 直接处理序列化)
      
  4. 优缺点:

    • 优点:可能保留大部分 Spock 测试代码结构和 given-when-then 语法。
    • 缺点:Mocking 非常困难且不优雅,失去了 Quarkus 测试框架的核心优势(如 @InjectMock),维护成本高,可能遇到意想不到的集成问题。基本违背了使用 Quarkus 提供的便捷测试工具的初衷。

方案二:迁移到 JUnit 5(推荐)

这是 Quarkus 官方推荐且支持最好的方式。虽然需要修改测试代码,但长期来看更稳定、更易维护,并且能充分利用 Quarkus 的测试特性。

  1. 目标: 将 Spock 测试重写为 JUnit 5 测试,使用 @QuarkusTest, REST Assured@InjectMock

  2. 原理: 利用 JUnit 5 作为测试运行器,@QuarkusTest 管理 Quarkus 应用生命周期和 CDI 容器,REST Assured 处理 HTTP 请求,@InjectMock (基于 Mockito) 无缝替换 CDI Bean 来进行 Mock 测试。

  3. 操作步骤与代码示例:

    • 添加依赖: 确保 pom.xmlbuild.gradlequarkus-junit5quarkus-junit5-mockitorest-assured 以及相关的 JAX-RS 和 JSON 依赖。移除 quarkus-spockspock-core (如果不再需要)。

      <!-- pom.xml 示例 -->
      <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-junit5</artifactId>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-junit5-mockito</artifactId>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>io.rest-assured</groupId>
          <artifactId>rest-assured</artifactId>
          <scope>test</scope>
      </dependency>
      <!-- JAX-RS & JSON dependencies... -->
      
    • 创建 JUnit 5 测试类: 创建一个新的 Java 或 Kotlin 类 (虽然技术上 JUnit 5 也能运行 Groovy,但通常大家会用 Java/Kotlin 写 Quarkus 测试)。

    • 使用注解:

      • 在类上加 @QuarkusTest
      • 注入要 Mock 的依赖,并使用 @InjectMock 注解。Quarkus 会自动为你创建 Mockito Mock 对象并注入到需要它的地方 (比如你的 ExampleResource Bean)。
      • 测试方法使用 JUnit 5 的 @Test 注解。
    • 编写测试逻辑:

      • 使用 REST Assured 发起 HTTP 请求。
      • 使用 Mockito 的 API (when(...).thenReturn(...), verify(...)) 来设置 Mock 行为和验证交互。
      • 断言使用 JUnit 5 的 Assertions 或 AssertJ 等库。
    • 示例 (JUnit 5 版本):

      import io.quarkus.test.junit.QuarkusTest;
      import io.quarkus.test.junit.mockito.InjectMock; // 关键注解
      import io.restassured.http.ContentType;
      import org.junit.jupiter.api.Test;
      import org.mockito.ArgumentMatcher;
      
      import javax.inject.Inject; // 如果需要注入真实 Bean (通常不用在 Mock 测试中)
      import javax.ws.rs.core.Response;
      
      import static io.restassured.RestAssured.given;
      import static org.hamcrest.Matchers.is;
      import static org.mockito.ArgumentMatchers.any;
      import static org.mockito.ArgumentMatchers.argThat;
      import static org.mockito.Mockito.verify;
      import static org.mockito.Mockito.when; // 如果需要 stub 返回值
      
      @QuarkusTest // 启动 Quarkus 应用
      public class ExampleResourceTest {
      
          static final String BASE_URL = "/myurl";
      
          // 使用 @InjectMock 替换 ExampleService Bean
          @InjectMock
          ExampleService service; // Quarkus 会自动注入一个 Mockito mock 对象
      
          // 通常不需要注入被测资源本身,测试其端点即可
          // @Inject ExampleResource resource;
      
          // 如果需要 Fixtures,可以类似地管理
          // private Fixtures fixtures = new Fixtures();
      
          @Test
          public void testPostRequestSuccess() {
              // given: 准备数据和 Mock 行为
              MyRequest request = createMyRequest(); // 假设有个创建请求的方法
      
              // 如果 service.handle 需要返回特定值,可以在这里 stub
              // when(service.handle(any(MyRequest.class))).thenReturn(someResult);
      
              // when: 使用 REST Assured 发起请求
              given()
                  .contentType(ContentType.JSON)
                  .body(request)
              .when()
                  .post(BASE_URL)
              .then()
                  // then: 验证 HTTP 响应状态码
                  .statusCode(Response.Status.OK.getStatusCode());
      
              // and: 使用 Mockito.verify 验证 service Mock 的交互
              verify(service).handle(argThat(new ArgumentMatcher<MyRequest>() {
                  @Override
                  public boolean matches(MyRequest actualRequest) {
                      // 实现 Spock 那样的属性匹配逻辑
                      return actualRequest.getField1().equals(request.getField1()) &&
                             actualRequest.getField2().equals(request.getField2()) &&
                             actualRequest.getField3().equals(request.getField3());
                  }
              }));
          }
      
          private MyRequest createMyRequest() {
              // ... 创建 MyRequest 实例 ...
              return new MyRequest(/* ... */);
          }
      }
      
      
  4. 关于 ResourceSpecification 基类: JUnit 5 支持测试类继承。你可以将一些通用的设置、辅助方法(如 createMyRequest)或者通用的 REST Assured 配置放到一个基类中,让具体的测试类继承它。

  5. REST Assured 用于单元测试? 你提到 REST Assured 主要用于集成测试。这没错。但 @QuarkusTest 启动的是一个(部分)应用实例。当你使用 @InjectMock 时,你实际上隔离了被测资源(ExampleResource)的依赖(ExampleService),使得测试焦点集中在 ExampleResource 本身处理 HTTP 请求、调用依赖(现在是 Mock)、返回响应的逻辑上。所以,它是一种处于单元测试和集成测试之间的测试 ,通常被称为“组件测试”或“服务测试”。它测试一个组件(Web 层)与外部(HTTP)和内部(Mocked 依赖)的交互,非常适合测试 Controller/Resource 层。

  6. 优缺点:

    • 优点:完全兼容 Quarkus 生态,享受所有测试特性(CDI、配置注入、 @InjectMock@TestProfile 等),Mocking 简单直接,社区支持好,长期维护性强。
    • 缺点:需要将 Spock 测试迁移到 JUnit 5/Mockito,有一次性的工作量。需要适应 JUnit 5/Mockito 的语法风格。

结论与建议

直接在 Spock 测试上使用 @QuarkusTest 来完美替代 JerseyTest 的方式,目前看来困难重重,主要卡在 Spock 与 Quarkus CDI (ArC) 的集成上。虽然理论上可能存在一些复杂的 workaround(如使用 Alternative Bean 或编程化的 QuarkusMock),但它们通常很笨拙,且容易出错,失去了使用现代框架应有的便捷性。

强烈建议选择方案二:迁移到 JUnit 5。

虽然这意味着需要投入时间重写测试,但这是融入 Quarkus 生态最顺畅、最能发挥其测试能力的方式。@QuarkusTest + REST Assured + @InjectMock 的组合是 Quarkus 官方推荐且功能强大的模式,可以让你编写出既能测试 Web 层交互逻辑,又能轻松隔离依赖的可靠测试。从长远来看,这将带来更高的开发效率和更好的可维护性。

至于重写成本,可以考虑:

  • 渐进式迁移: 先迁移核心业务或改动频繁部分的测试。
  • 利用工具或脚本: 虽然完全自动转换很难,但可以写一些简单的脚本来辅助转换基本结构(如注解替换、方法签名调整),减少手动工作量。
  • 共享代码: 将 Spock 中的一些辅助方法、Fixtures 逻辑提取出来,让新的 JUnit 5 测试也能复用。

最终选择哪条路取决于你的项目优先级、时间和资源。但如果追求与 Quarkus 的最佳集成和长期健康度,拥抱 JUnit 5 是更明智的选择。