Spock 迁移 Quarkus 测试:JerseyTest 替换 @QuarkusTest 详解
2025-03-27 19:19:01
用 @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 重写,工作量实在有点吓人。
尝试过的路子
-
REST Assured + 手动启动服务器: 试过用
REST Assured
替换JerseyTest
来发 HTTP 请求,同时用GrizzlyHttpServerFactory
手动搭了个服务器跑资源。效果不错,测试能跑通。但这违背了初衷——我们想用 Quarkus 的能力,而不是自己搞一套服务器启动逻辑,还引入了额外的 Grizzly 依赖。 -
REST Assured + @QuarkusTest (失败): 把手动启动服务器的代码干掉,在
ResourceSpecification
类上加了@QuarkusTest
注解,期望它能启动 Quarkus 服务器。结果,REST Assured
发请求时直接报ConnectionException
,连不上服务器。
感觉问题出在 Spock 和 Quarkus 的 CDI 容器 ArC 之间缺乏像 JUnit 5 那样的“官方”集成。
问题分析:为什么直接替换行不通?
@QuarkusTest
和 JerseyTest
的工作方式有本质区别。
-
JerseyTest
: 它主要关注 JAX-RS 层面。它启动一个轻量级服务器(默认 Grizzly),加载你明确指定的 JAX-RSResource
类和Application
配置。它不关心完整的应用上下文,比如 CDI 容器、配置注入等,除非你在ResourceConfig
里手动配置。在你的例子里,createResource()
返回的资源实例,以及其依赖service
(被 Spock Mock 了),都是在测试代码里手动创建和管理的,和 CDI 关系不大。 -
@QuarkusTest
: 这个注解启动的是一个几乎完整的 Quarkus 应用。它会初始化 CDI 容器 (ArC),扫描和管理 Bean,处理@Inject
,加载application.properties
配置等等。它的目标是提供一个尽可能接近生产环境的测试运行时。
当你尝试在 Spock 测试类上用 @QuarkusTest
时,会遇到几个核心障碍:
-
生命周期不匹配: Spock 的测试实例生命周期管理和 JUnit 5 (Quarkus 主要支持的) 不一样。
@QuarkusTest
的底层机制是为 JUnit 5 设计的,用来在测试前后启动和关闭 Quarkus 应用。它可能无法正确地与 Spock 的setup()
和cleanup()
(或者@BeforeAll
/@AfterAll
对应的setupSpec()
/cleanupSpec()
) 方法协调工作。 -
CDI 集成缺失: 这是关键。Quarkus 的测试框架与 ArC 深度集成。在 JUnit 5 测试中,你可以用
@Inject
来注入 Quarkus 管理的 Bean,用@InjectMock
来轻松替换某个 Bean 为 Mockito mock 对象。Spock 测试默认情况下享受不到这些便利。即使@QuarkusTest
成功启动了服务器,Spock 测试本身也无法直接利用 Quarkus 的 CDI 功能来获取或模拟 Bean。你那个手动创建ExampleResource
实例并传入 Spock Mockservice
的做法,绕过了 Quarkus 的 CDI 容器。当@QuarkusTest
启动时,Quarkus 容器可能也尝试根据自己的规则去发现和创建ExampleResource
(如果它也被注解为 CDI Bean 的话),这可能导致冲突或行为不符合预期。REST Assured
连不上服务器,很可能是因为 Quarkus 应用没能按预期完全启动,或者启动的 JAX-RS 端点配置跟你手动创建资源时期望的不一致。 -
依赖注入问题: Spock 的
@Shared
和Mock()
是 Spock 自己的机制。它们创建的对象对于 Quarkus 的 ArC 容器来说是“外部”的。当你的ExampleResource
作为一个 Quarkus Bean 被容器管理时,它期望的依赖(比如ExampleService
)会由 ArC 来注入。这时,Spock 的 Mock 对象就插不进去了,除非使用 Quarkus 提供的@InjectMock
(但这又回到了需要 JUnit 5 集成的问题)。
解决方案探讨
摆在面前的路似乎有两条,各有优劣。
方案一:坚持使用 Spock(有代价)
这条路比较坎坷,因为官方集成度不高。你需要接受一些限制,或者进行更复杂的配置。
-
目标: 让
@QuarkusTest
启动 Quarkus 应用,用REST Assured
发请求,但 Mocking 机制可能需要调整。 -
原理: 尝试利用
@QuarkusTest
把 Quarkus 应用跑起来,这样 JAX-RS 端点就能用了。然后用REST Assured
(一个纯 HTTP 客户端) 去访问这些端点。核心挑战在于如何处理依赖 Mock。 -
操作步骤与代码示例:
-
添加依赖: 确保
pom.xml
或build.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()
里创建的 Mockservice
是注入到手动创建的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 直接处理序列化)
-
-
优缺点:
- 优点:可能保留大部分 Spock 测试代码结构和
given-when-then
语法。 - 缺点:Mocking 非常困难且不优雅,失去了 Quarkus 测试框架的核心优势(如
@InjectMock
),维护成本高,可能遇到意想不到的集成问题。基本违背了使用 Quarkus 提供的便捷测试工具的初衷。
- 优点:可能保留大部分 Spock 测试代码结构和
方案二:迁移到 JUnit 5(推荐)
这是 Quarkus 官方推荐且支持最好的方式。虽然需要修改测试代码,但长期来看更稳定、更易维护,并且能充分利用 Quarkus 的测试特性。
-
目标: 将 Spock 测试重写为 JUnit 5 测试,使用
@QuarkusTest
,REST Assured
和@InjectMock
。 -
原理: 利用 JUnit 5 作为测试运行器,
@QuarkusTest
管理 Quarkus 应用生命周期和 CDI 容器,REST Assured
处理 HTTP 请求,@InjectMock
(基于 Mockito) 无缝替换 CDI Bean 来进行 Mock 测试。 -
操作步骤与代码示例:
-
添加依赖: 确保
pom.xml
或build.gradle
有quarkus-junit5
,quarkus-junit5-mockito
,rest-assured
以及相关的 JAX-RS 和 JSON 依赖。移除quarkus-spock
和spock-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(/* ... */); } }
-
-
关于
ResourceSpecification
基类: JUnit 5 支持测试类继承。你可以将一些通用的设置、辅助方法(如createMyRequest
)或者通用的REST Assured
配置放到一个基类中,让具体的测试类继承它。 -
REST Assured 用于单元测试? 你提到
REST Assured
主要用于集成测试。这没错。但@QuarkusTest
启动的是一个(部分)应用实例。当你使用@InjectMock
时,你实际上隔离了被测资源(ExampleResource
)的依赖(ExampleService
),使得测试焦点集中在ExampleResource
本身处理 HTTP 请求、调用依赖(现在是 Mock)、返回响应的逻辑上。所以,它是一种处于单元测试和集成测试之间的测试 ,通常被称为“组件测试”或“服务测试”。它测试一个组件(Web 层)与外部(HTTP)和内部(Mocked 依赖)的交互,非常适合测试 Controller/Resource 层。 -
优缺点:
- 优点:完全兼容 Quarkus 生态,享受所有测试特性(CDI、配置注入、
@InjectMock
、@TestProfile
等),Mocking 简单直接,社区支持好,长期维护性强。 - 缺点:需要将 Spock 测试迁移到 JUnit 5/Mockito,有一次性的工作量。需要适应 JUnit 5/Mockito 的语法风格。
- 优点:完全兼容 Quarkus 生态,享受所有测试特性(CDI、配置注入、
结论与建议
直接在 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 是更明智的选择。