1. 概述
如今,Quarkus 让我们能快速构建健壮且结构清晰的应用。但测试呢?它支持得怎么样?
本文将深入探讨 如何对 Quarkus 应用进行有效测试。我们会覆盖 Quarkus 提供的核心测试能力,包括依赖注入与管理、Mock 技术、测试配置剖面(Profile)、Quarkus 特有的注解用法,甚至如何测试原生可执行文件(Native Executable)。
对于有经验的开发者来说,这些内容能帮你避开常见“坑位”,写出更可靠、更高效的测试代码。
2. 环境准备
我们基于之前搭建的 QuarkusIO 项目 进行扩展。
首先,添加以下 Maven 依赖:
✅ 关键依赖说明:
quarkus-resteasy-jackson
:提供 REST 接口支持与 JSON 序列化quarkus-hibernate-orm-panache
:简化 JPA 操作quarkus-jdbc-h2
+quarkus-test-h2
:嵌入式数据库支持,测试专用quarkus-junit5-mockito
:集成 JUnit 5 与 Mockito
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
</dependency>
接下来,定义领域模型:
public class Book extends PanacheEntity {
private String title;
private String author;
}
创建 Panache 风格的 Repository,支持模糊查询:
public class BookRepository implements PanacheRepository {
public Stream<Book> findBy(String query) {
return find("author like :query or title like :query", with("query", "%"+query+"%")).stream();
}
}
封装业务逻辑的服务层:
public class LibraryService {
@Inject
BookRepository bookRepository;
public Set<Book> find(String query) {
if (query == null) {
return bookRepository.findAll().stream().collect(toSet());
}
return bookRepository.findBy(query).collect(toSet());
}
}
最后,通过 REST 接口暴露服务:
@Path("/library")
public class LibraryResource {
@Inject
LibraryService libraryService;
@GET
@Path("/book")
public Set<Book> findBooks(@QueryParam("query") String query) {
return libraryService.find(query);
}
}
3. 使用 @Alternative 提供测试专用实现
在写测试前,我们希望数据库里有初始数据。Quarkus 的 CDI 支持通过 @Alternative
注解提供测试专用 Bean。
创建一个 TestBookRepository
,继承自 BookRepository
,并在初始化时插入测试数据:
@Priority(1)
@Alternative
@ApplicationScoped
public class TestBookRepository extends BookRepository {
@PostConstruct
public void init() {
persist(
new Book("Dune", "Frank Herbert"),
new Book("Foundation", "Isaac Asimov")
);
}
}
⚠️ 注意:
- 将此类放在
test
目录下 @Priority(1)
确保测试时优先使用该实现@Alternative
标记为可替换实现
这是一种全局 Mock 策略,所有测试都会使用这个预填充数据的 Repository。后面我们会介绍更细粒度的 Mock 方式。
4. HTTP 集成测试
使用 @QuarkusTest
注解可启动完整的 Quarkus 应用上下文,适合测试 REST 接口。
@QuarkusTest
class LibraryResourceIntegrationTest {
@Test
void whenGetBooksByTitle_thenBookShouldBeFound() {
given()
.contentType(ContentType.JSON)
.param("query", "Dune")
.when()
.get("/library/book")
.then()
.statusCode(200)
.body("size()", is(1))
.body("title", hasItem("Dune"))
.body("author", hasItem("Frank Herbert"));
}
}
✅ @QuarkusTest
会自动启动应用,测试结束后关闭。
4.1 使用 @TestHTTPResource 注入接口地址
避免硬编码 URL,使用 @TestHTTPResource
注入:
@TestHTTPResource("/library/book")
URL libraryEndpoint;
在测试中使用:
@Test
void whenGetBooksByTitle_thenBookShouldBeFound() {
given()
.param("query", "Dune")
.when()
.get(libraryEndpoint)
.then()
.statusCode(200);
}
甚至可以直接读取流:
@Test
void whenGetBooks_thenBooksShouldBeFound() throws IOException {
assertTrue(
IOUtils.toString(libraryEndpoint.openStream(), defaultCharset())
.contains("Asimov")
);
}
4.2 使用 @TestHTTPEndpoint 简化路径管理
更进一步,使用 @TestHTTPEndpoint
基于资源类自动推导路径:
@TestHTTPEndpoint(LibraryResource.class)
@TestHTTPResource("book")
URL libraryEndpoint;
或者直接在类上标注,让 REST-assured 自动拼接路径:
@QuarkusTest
@TestHTTPEndpoint(LibraryResource.class)
class LibraryHttpEndpointIntegrationTest {
@Test
void whenGetBooks_thenShouldReturnSuccessfully() {
given()
.contentType(ContentType.JSON)
.when()
.get("book") // 自动变成 /library/book
.then()
.statusCode(200);
}
}
✅ 这种方式解耦了测试与具体路径,重构时更安全。
5. 上下文与依赖注入(CDI)
Quarkus 测试本身就是 CDI Bean,因此可以使用 @Inject
注入任何 Bean。
测试 LibraryService
:
@QuarkusTest
class LibraryServiceIntegrationTest {
@Inject
LibraryService libraryService;
@Test
void whenFindByAuthor_thenBookShouldBeFound() {
assertFalse(libraryService.find("Frank Herbert").isEmpty());
}
}
测试 BookRepository
时需要注意事务:
@QuarkusTransactionalTest
class BookRepositoryIntegrationTest {
@Inject
BookRepository bookRepository;
@Test
void givenBookInRepository_whenFindByAuthor_thenShouldReturnBookFromRepository() {
assertTrue(bookRepository.findBy("Herbert").findAny().isPresent());
}
}
⚠️ 失败原因:缺少事务上下文。解决方案:
创建自定义注解,组合 @QuarkusTest
与 @Transactional
:
@QuarkusTest
@Stereotype
@Transactional
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuarkusTransactionalTest {
}
✅ 这样以后所有需要事务的测试都可以直接使用 @QuarkusTransactionalTest
,简洁又统一。
6. Mock 技术
Mock 是测试的核心。Quarkus 提供了多种方式,从全局替换到局部 Mock。
6.1 @Mock 注解
@Mock
是 @Alternative
的简化版,内部组合了 @Alternative
和 @Priority(1)
,使用更简洁。
6.2 @QuarkusMock:动态注册 Mock
如果不想定义额外类,可以在测试中动态创建 Mock:
@QuarkusTest
class LibraryServiceQuarkusMockUnitTest {
@Inject
LibraryService libraryService;
@BeforeEach
void setUp() {
BookRepository mock = Mockito.mock(BookRepository.class);
when(mock.findBy("Asimov"))
.thenReturn(Stream.of(
new Book("Foundation", "Isaac Asimov"),
new Book("I Robot", "Isaac Asimov")
));
QuarkusMock.installMockForType(mock, BookRepository.class);
}
@Test
void whenFindByAuthor_thenBooksShouldBeFound() {
assertEquals(2, libraryService.find("Asimov").size());
}
}
✅ 适合临时 Mock,无需创建额外类。
6.3 @InjectMock:最简洁的字段级 Mock
更推荐使用 @InjectMock
,直接在字段上声明 Mock:
@QuarkusTest
class LibraryServiceInjectMockUnitTest {
@Inject
LibraryService libraryService;
@InjectMock
BookRepository bookRepository;
@BeforeEach
void setUp() {
when(bookRepository.findBy("Frank Herbert"))
.thenReturn(Stream.of(
new Book("Dune", "Frank Herbert"),
new Book("Children of Dune", "Frank Herbert")
));
}
@Test
void whenFindByAuthor_thenBooksShouldBeFound() {
assertEquals(2, libraryService.find("Frank Herbert").size());
}
}
✅ 写法最简单,语义清晰,推荐日常使用。
6.4 @InjectSpy:仅监控不替换
如果只想验证方法是否被调用,而不替换行为,使用 @InjectSpy
:
@QuarkusTest
class LibraryResourceInjectSpyIntegrationTest {
@InjectSpy
LibraryService libraryService;
@Test
void whenGetBooksByAuthor_thenBookShouldBeFound() {
given()
.contentType(ContentType.JSON)
.param("query", "Asimov")
.when()
.get("/library/book")
.then()
.statusCode(200);
verify(libraryService).find("Asimov");
}
}
✅ 适合验证接口调用链,保持原有逻辑执行。
7. 测试配置剖面(Test Profiles)
有时我们需要在不同配置下运行测试,比如切换数据库、修改接口路径等。Quarkus 的 TestProfile
正是为此设计。
实现 QuarkusTestProfile
接口:
public class CustomTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Collections.singletonMap("quarkus.resteasy.path", "/custom");
}
@Override
public Set<Class<?>> getEnabledAlternatives() {
return Collections.singleton(TestBookRepository.class);
}
@Override
public String getConfigProfile() {
return "custom-profile";
}
}
在 application.properties
中添加剖面配置:
%custom-profile.quarkus.datasource.jdbc.url=jdbc:h2:file:./testdb
编写测试:
@QuarkusTest
@TestProfile(CustomTestProfile.class)
class CustomLibraryResourceManualTest {
public static final String BOOKSTORE_ENDPOINT = "/custom/library/book";
@Test
void whenGetBooksGivenNoQuery_thenAllBooksShouldBeReturned() {
given()
.contentType(ContentType.JSON)
.when()
.get(BOOKSTORE_ENDPOINT)
.then()
.statusCode(200)
.body("size()", is(2))
.body("title", hasItems("Foundation", "Dune"));
}
}
⚠️ 注意:
- 测试前 Quarkus 会重启应用以加载新剖面
- 代价是启动时间变长,但换来极大的灵活性
8. 测试原生可执行文件
Quarkus 支持将应用编译为原生镜像(Native Image),并提供专用测试支持。
@QuarkusIntegrationTest
@QuarkusTestResource(H2DatabaseTestResource.class)
class NativeLibraryResourceIT extends LibraryHttpEndpointIntegrationTest {
}
执行构建与测试:
mvn verify -Pnative
✅ 关键点说明:
@QuarkusIntegrationTest
:指示测试运行在原生镜像上@QuarkusTestResource
:启动外部服务(如数据库),因为原生镜像不嵌入数据库- 测试代码本身不运行在原生环境中,只有被测应用是原生的
你还需要安装 GraalVM 来构建原生镜像。
扩展用法:通过实现 QuarkusTestResourceLifecycleManager
接口,可启动自定义资源,如 TestContainers:
@QuarkusTestResource(OurCustomResourceImpl.class)
9. 总结
Quarkus 提供了极其强大且灵活的测试支持。从基础的依赖注入、Mock,到高级的测试剖面、原生镜像测试,几乎覆盖了所有场景。
✅ 核心优势:
- 测试即 CDI Bean,无缝集成
- 多种 Mock 方式,按需选择
- 支持复杂环境模拟
- 原生测试能力领先
完整代码已上传至 GitHub,建议结合实践加深理解。