测试您的应用程序
了解如何测试您的 Quarkus 应用程序。本指南涵盖
-
JVM 模式下的测试
-
原生模式下的测试
-
将资源注入测试
1. 前提条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
-
《入门指南》中已完成的 greeter 应用程序
3. 解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载一个存档。
解决方案位于 getting-started-testing
目录中。
本指南假定您已经拥有 getting-started
目录中已完成的应用程序。
4. JVM 模式下基于 HTTP 的测试回顾
如果您从入门示例开始,应该已经有一个完整的测试,包括正确的工具设置。
在您的构建文件中,您应该看到 2 个测试依赖项
quarkus-junit5
是测试所必需的,因为它提供了 @QuarkusTest
注释来控制测试框架。rest-assured
不是必需的,但它是测试 HTTP 端点的便捷方式,我们还提供了集成,可自动设置正确的 URL,因此无需进行配置。
由于我们使用 JUnit 5,因此必须设置 Surefire Maven Plugin 的版本,因为默认版本不支持 Junit 5
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
我们还设置了 java.util.logging.manager
系统属性,以确保测试使用正确的日志管理器,并设置 maven.home
以确保应用了来自 ${maven.home}/conf/settings.xml
的自定义配置(如果有)。
项目还应包含一个简单的测试
package org.acme.getting.started.testing;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class GreetingResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello"));
}
@Test
public void testGreetingEndpoint() {
String uuid = UUID.randomUUID().toString();
given()
.pathParam("name", uuid)
.when().get("/hello/greeting/{name}")
.then()
.statusCode(200)
.body(is("hello " + uuid));
}
}
此测试使用 HTTP 直接测试我们的 REST 端点。运行测试时,应用程序将在测试运行之前启动。
4.1. 控制测试端口
虽然 Quarkus 默认会监听 8080 端口,但在运行测试时,它默认为 8081。这允许您在应用程序并行运行时运行测试。
更改测试端口
您可以通过在
|
Quarkus 还提供了 RestAssured 集成,该集成会在测试运行之前更新 RestAssured 使用的默认端口,因此无需其他配置。
4.2. 控制 HTTP 交互超时
在测试中使用 RESTAssured 时,连接和响应超时均设置为 30 秒。您可以通过 quarkus.http.test-timeout
属性覆盖此设置。
quarkus.http.test-timeout=10s
4.3. 注入 URI
还可以直接将 URL 注入测试,这可以方便地使用不同的客户端。这通过 @TestHTTPResource
注释完成。
让我们编写一个简单的测试来演示加载一些静态资源。首先,在 src/main/resources/META-INF/resources/index.html
中创建一个简单的 HTML 文件。
<html>
<head>
<title>Testing Guide</title>
</head>
<body>
Information about testing
</body>
</html>
我们将创建一个简单的测试来确保其被正确提供。
package org.acme.getting.started.testing;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class StaticContentTest {
@TestHTTPResource("index.html") (1)
URL url;
@Test
public void testIndexHtml() throws IOException {
try (InputStream in = url.openStream()) {
String contents = new String(in.readAllBytes(), StandardCharsets.UTF_8);
Assertions.assertTrue(contents.contains("<title>Testing Guide</title>"));
}
}
}
1 | 此注释允许您直接注入 Quarkus 实例的 URL,注释的值将是 URL 的路径组件。 |
目前 @TestHTTPResource
允许您注入 URL 的 URI
、URL
和 String
表示形式。
5. 测试特定端点
RESTassured 和 @TestHTTPResource
都允许您指定要测试的端点类,而不是硬编码路径。目前支持 Jakarta REST 端点、Servlet 和 Reactive Routes。这使得查看给定测试正在测试哪些端点变得更加容易。
为了这些示例的目的,我将假定我们有一个如下所示的端点
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
这目前不支持 @ApplicationPath() 注释来设置 Jakarta REST 上下文路径。如果需要自定义上下文路径,请改用 quarkus.resteasy.path 配置值。 |
5.1. TestHTTPResource
您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint
注释来指定端点路径,并且路径将从提供的端点中提取。如果您还为 TestHTTPResource
端点指定了值,它将附加到端点路径的末尾。
package org.acme.getting.started.testing;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class StaticContentTest {
@TestHTTPEndpoint(GreetingResource.class) (1)
@TestHTTPResource
URL url;
@Test
public void testIndexHtml() throws IOException {
try (InputStream in = url.openStream()) {
String contents = new String(in.readAllBytes(), StandardCharsets.UTF_8);
Assertions.assertEquals("hello", contents);
}
}
}
1 | 因为 GreetingResource 用 @Path("/hello") 进行了注释,所以注入的 URL 将以 /hello 结尾。 |
5.2. RESTassured
要控制 RESTassured 的基本路径(即作为每个请求根的默认路径),您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint
注释。这可以应用于类或方法级别。为了测试 greeting 资源,我们将这样做
package org.acme.getting.started.testing;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
@TestHTTPEndpoint(GreetingResource.class) (1)
public class GreetingResourceTest {
@Test
public void testHelloEndpoint() {
when().get() (2)
.then()
.statusCode(200)
.body(is("hello"));
}
}
1 | 这告诉 RESTAssured 将所有请求都以 /hello 作为前缀。 |
2 | 请注意,我们无需在此处指定路径,因为 /hello 是此测试的默认值。 |
6. 将内容注入测试
到目前为止,我们只涵盖了通过 HTTP 端点测试应用程序的集成式测试,但是如果我们想进行单元测试并直接测试我们的 bean 怎么办?
Quarkus 通过允许您通过 @Inject
注释将 CDI bean 注入到测试中来支持这一点(事实上,Quarkus 中的测试是完整的 CDI bean,因此您可以使用所有 CDI 功能)。让我们创建一个简单的测试来直接测试 greeting 服务,而无需使用 HTTP。
package org.acme.getting.started.testing;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class GreetingServiceTest {
@Inject (1)
GreetingService service;
@Test
public void testGreetingService() {
Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));
}
}
1 | GreetingService bean 将被注入到测试中。 |
如果您想注入/测试一个 @SessionScoped bean,那么会话上下文很可能不处于活动状态,并且在调用注入 bean 的方法时,您会收到 ContextNotActiveException 。但是,可以使用 @io.quarkus.test.ActivateSessionContext 拦截器绑定来为特定业务方法激活会话上下文。请阅读 javadoc 以了解进一步的限制。 |
7. 将拦截器应用于测试
如上所述,Quarkus 测试实际上是完整的 CDI bean,因此您可以像平常一样应用 CDI 拦截器。例如,如果您希望某个测试方法在事务上下文中运行,您可以简单地将 @Transactional
注释应用于该方法,事务拦截器将负责处理它。
此外,您还可以创建自己的测试注解。例如,我们可以创建一个 @TransactionalQuarkusTest
,如下所示:
@QuarkusTest
@Stereotype
@Transactional
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TransactionalQuarkusTest {
}
如果我们然后将此注释应用于测试类,它将表现得好像我们应用了 @QuarkusTest
和 @Transactional
注释一样,例如:
@TransactionalQuarkusTest
public class TestStereotypeTestCase {
@Inject
UserTransaction userTransaction;
@Test
public void testUserTransaction() throws Exception {
Assertions.assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus());
}
}
8. 测试和事务
您可以在测试上使用标准的 Quarkus @Transactional
注释,但这表示您的测试对数据库所做的更改将是持久的。如果您希望在测试结束时回滚任何更改,可以使用 io.quarkus.test.TestTransaction
注释。这将使测试方法在事务中运行,但在测试方法完成后回滚该事务,以撤销任何数据库更改。
9. 通过 QuarkusTest*Callback 进行增强
作为拦截器的替代或补充,您可以通过实现以下回调接口来增强所有 @QuarkusTest
类。
-
io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback
-
io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback
-
io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback
-
io.quarkus.test.junit.callback.QuarkusTestBeforeTestExecutionCallback
-
io.quarkus.test.junit.callback.QuarkusTestAfterTestExecutionCallback
-
io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
可选地,如果属性 quarkus.test.enable-callbacks-for-integration-tests
为 true
,您也可以为 @QuarkusIntegrationTest
测试启用这些回调。
此类回调实现必须注册为 java.util.ServiceLoader
定义的“服务提供商”。
例如,以下示例回调:
package org.acme.getting.started.testing;
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {
@Override
public void beforeEach(QuarkusTestMethodContext context) {
System.out.println("Executing " + context.getTestMethod());
}
}
必须通过 src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback
注册,如下所示:
org.acme.getting.started.testing.MyQuarkusTestBeforeEachCallback
可以读取测试类或方法的注释来控制回调的操作。 |
10. 测试不同的配置文件
到目前为止,在我们所有的示例中,我们只为所有测试启动一次 Quarkus。在运行第一个测试之前,Quarkus 将启动,然后所有测试将运行,最后 Quarkus 将在结束时关闭。这提供了非常快速的测试体验,但它有些有限,因为您无法测试不同的配置。
为了解决这个问题,Quarkus 支持测试配置文件的概念。如果一个测试的配置文件与之前运行的测试不同,那么 Quarkus 将会关闭并使用新配置文件启动,然后再运行测试。这显然会慢一些,因为它会在测试时间中添加一个关闭/启动周期,但提供了很大的灵活性。
为了减少 Quarkus 需要重新启动的次数,io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer
被注册为一个全局 ClassOrderer
,如 JUnit 5 用户指南中所述。此 ClassOrderer
的行为可以通过 application.properties
使用属性 quarkus.test.class-orderer
进行配置。该属性接受 ClassOrderer
的 FQCN。如果找不到该类,它将回退到 JUnit 的默认行为,即根本不设置 ClassOrderer
。您也可以通过设置 JUnit 5 提供的另一个 ClassOrderer
或您自己的自定义 ClassOrderer
来完全禁用它。
10.1. 编写配置文件
要实现测试配置文件,我们需要实现 io.quarkus.test.junit.QuarkusTestProfile
。
package org.acme.getting.started.testing;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.enterprise.inject.Produces;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
public class MockGreetingProfile implements QuarkusTestProfile { (1)
/**
* Returns additional config to be applied to the test. This
* will override any existing config (including in application.properties),
* however existing config will be merged with this (i.e. application.properties
* config will still take effect, unless a specific config key has been overridden).
*
* Here we are changing the Jakarta REST root path.
*/
@Override
public Map<String, String> getConfigOverrides() {
return Collections.singletonMap("quarkus.resteasy.path","/api");
}
/**
* Returns enabled alternatives.
*
* This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key,
* however it may be more convenient.
*/
@Override
public Set<Class<?>> getEnabledAlternatives() {
return Collections.singleton(MockGreetingService.class);
}
/**
* Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system
* property before the test is run.
*
* Here we are setting the profile to test-mocked
*/
@Override
public String getConfigProfile() {
return "test-mocked";
}
/**
* Additional {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this
* specific test profile.
*
* If this method is not overridden, then only the {@link QuarkusTestResourceLifecycleManager} classes enabled via the {@link io.quarkus.test.common.QuarkusTestResource} class
* annotation will be used for the tests using this profile (which is the same behavior as tests that don't use a profile at all).
*/
@Override
public List<TestResourceEntry> testResources() {
return Collections.singletonList(new TestResourceEntry(CustomWireMockServerManager.class));
}
/**
* If this returns true then only the test resources returned from {@link #testResources()} will be started,
* global annotated test resources will be ignored.
*/
@Override
public boolean disableGlobalTestResources() {
return false;
}
/**
* The tags this profile is associated with.
* When the {@code quarkus.test.profile.tags} System property is set (its value is a comma separated list of strings)
* then Quarkus will only execute tests that are annotated with a {@code @TestProfile} that has at least one of the
* supplied (via the aforementioned system property) tags.
*/
@Override
public Set<String> tags() {
return Collections.emptySet();
}
/**
* The command line parameters that are passed to the main method on startup.
*/
@Override
public String[] commandLineParameters() {
return new String[0];
}
/**
* If the main method should be run.
*/
@Override
public boolean runMainMethod() {
return false;
}
/**
* If this method returns true then all {@code StartupEvent} and {@code ShutdownEvent} observers declared on application
* beans should be disabled.
*/
@Override
public boolean disableApplicationLifecycleObservers() {
return false;
}
@Produces (2)
public ExternalService mockExternalService() {
return new ExternalService("mock");
}
}
1 | 所有这些方法都有默认实现,所以只需覆盖您需要覆盖的方法即可。 |
2 | 如果测试配置文件实现声明了一个 CDI bean(通过生产者方法/字段或嵌套静态类),那么该 bean 仅在该测试配置文件使用时才会被考虑,也就是说,它会被任何其他测试配置文件忽略。 |
现在我们已经定义了配置文件,我们需要将其包含在我们的测试类中。我们通过用 @TestProfile(MockGreetingProfile.class)
注释测试类来完成此操作。
所有测试配置都存储在一个类中,这使得很容易判断之前的测试是否使用了相同的配置。
10.2. 运行特定测试
Quarkus 提供了将测试执行限制为具有特定 @TestProfile
注释的测试的能力。这通过将 QuarkusTestProfile
的 tags
方法与 quarkus.test.profile.tags
系统属性结合使用来实现。
本质上,任何具有至少一个与 quarkus.test.profile.tags
值匹配的标签的 QuarkusTestProfile
都将被视为活动状态,而所有用 @TestProfile
注释的活动配置文件将运行,其余的将被跳过。这在以下示例中得到了最好的体现。
首先,让我们像这样定义几个 QuarkusTestProfile
实现。
public class Profiles {
public static class NoTags implements QuarkusTestProfile {
}
public static class SingleTag implements QuarkusTestProfile {
@Override
public Set<String> tags() {
return Set.of("test1");
}
}
public static class MultipleTags implements QuarkusTestProfile {
@Override
public Set<String> tags() {
return Set.of("test1", "test2");
}
}
}
现在,让我们假设我们有以下测试。
@QuarkusTest
public class NoQuarkusProfileTest {
@Test
public void test() {
// test something
}
}
@QuarkusTest
@TestProfile(Profiles.NoTags.class)
public class NoTagsTest {
@Test
public void test() {
// test something
}
}
@QuarkusTest
@TestProfile(Profiles.SingleTag.class)
public class SingleTagTest {
@Test
public void test() {
// test something
}
}
@QuarkusTest
@TestProfile(Profiles.MultipleTags.class)
public class MultipleTagsTest {
@Test
public void test() {
// test something
}
}
让我们考虑以下场景。
-
未设置
quarkus.test.profile.tags
:所有测试都将执行。 -
quarkus.test.profile.tags=foo
:在这种情况下,将不执行任何测试,因为QuarkusTestProfile
实现上定义的任何标签均不匹配quarkus.test.profile.tags
的值。请注意,NoQuarkusProfileTest
也未执行,因为它没有用@TestProfile
进行注释。 -
quarkus.test.profile.tags=test1
:在这种情况下,将运行SingleTagTest
和MultipleTagsTest
,因为它们各自QuarkusTestProfile
实现上的标签与quarkus.test.profile.tags
的值匹配。 -
quarkus.test.profile.tags=test1,test3
:这种情况的结果与前一种情况相同,都执行了相同的测试。 -
quarkus.test.profile.tags=test2,test3
:在这种情况下,将仅运行MultipleTagsTest
,因为MultipleTagsTest
是唯一一个QuarkusTestProfile
实现的tags
方法与quarkus.test.profile.tags
的值匹配。
11. 嵌套测试
JUnit 5 @Nested tests 对于构建更复杂的测试场景很有用。但是,请注意,无法在同一父类中为嵌套测试分配不同的测试配置文件或资源。
12. Mock 支持
Quarkus 支持使用两种不同的方法使用 mock 对象。您可以覆盖 bean 以 mockout 所有测试类的 bean,或者使用 QuarkusMock
以每个测试为基础 mockout bean。
12.1. CDI @Alternative
机制。
要使用此方法,只需将要 mock 的 bean 替换为 src/test/java
目录中的类,并在 bean 上放置 @Alternative
和 @Priority(1)
注释。或者,可以使用方便的 io.quarkus.test.Mock
stereotype 注释。此内置 stereotype 声明了 @Alternative
、@Priority(1)
和 @Dependent
。例如,如果我有以下服务:
@ApplicationScoped
public class ExternalService {
public String service() {
return "external";
}
}
我可以用 src/test/java
中的以下类来 mock 它:
@Mock
@ApplicationScoped (1)
public class MockExternalService extends ExternalService {
@Override
public String service() {
return "mock";
}
}
1 | 覆盖 @Mock stereotype 上声明的 @Dependent 范围。 |
重要的是,替代项必须存在于 src/test/java
目录中,而不是 src/main/java
中,否则它将一直生效,而不仅仅是在测试时。
请注意,此方法目前不适用于原生映像测试,因为这需要将测试替代项烘焙到原生映像中。
12.2. 使用 QuarkusMock 进行 Mock
io.quarkus.test.junit.QuarkusMock
类可用于临时 mock 任何正常范围的 bean。如果您在 @BeforeAll
方法中使用此方法,则 mock 将在当前类的所有测试中生效,而如果您在测试方法中使用此方法,则 mock 将仅在当前测试期间生效。
此方法可用于任何正常范围的 CDI bean(例如 @ApplicationScoped
、@RequestScoped
等,基本上除 @Singleton
和 @Dependent
之外的所有范围)。
使用示例可能如下:
@QuarkusTest
public class MockTestCase {
@Inject
MockableBean1 mockableBean1;
@Inject
MockableBean2 mockableBean2;
@BeforeAll
public static void setup() {
MockableBean1 mock = Mockito.mock(MockableBean1.class);
Mockito.when(mock.greet("Stuart")).thenReturn("A mock for Stuart");
QuarkusMock.installMockForType(mock, MockableBean1.class); (1)
}
@Test
public void testBeforeAll() {
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals("Hello Stuart", mockableBean2.greet("Stuart"));
}
@Test
public void testPerTestMock() {
QuarkusMock.installMockForInstance(new BonjourGreeter(), mockableBean2); (2)
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
}
@ApplicationScoped
public static class MockableBean1 {
public String greet(String name) {
return "Hello " + name;
}
}
@ApplicationScoped
public static class MockableBean2 {
public String greet(String name) {
return "Hello " + name;
}
}
public static class BonjourGreeter extends MockableBean2 {
@Override
public String greet(String name) {
return "Bonjour " + name;
}
}
}
1 | 由于此处未提供注入的实例,我们使用 installMockForType ,此 mock 用于两个测试方法。 |
2 | 我们使用 installMockForInstance 来替换注入的 bean,这会在测试方法期间生效。 |
请注意,没有 Mockito 的依赖项,您可以使用任何您喜欢的 mock 库,甚至可以手动覆盖对象以提供所需行为。
使用 @Inject 将获得 mock 实例的 CDI 代理,这不适合传递给 Mockito.verify 等方法,因为这些方法需要 mock 实例本身。因此,如果您需要调用 verify 等方法,您应该在测试中保留 mock 实例,或使用 @io.quarkus.test.InjectMock 。 |
12.2.1. 使用 @InjectMock
进一步简化
基于 QuarkusMock
提供的功能,Quarkus 还允许用户轻松利用 Mockito 来 mock QuarkusMock
支持的 bean。
此功能可在
|
使用 @InjectMock
,前面的示例可以这样编写:
@QuarkusTest
public class MockTestCase {
@InjectMock
MockableBean1 mockableBean1; (1)
@InjectMock
MockableBean2 mockableBean2;
@BeforeEach
public void setup() {
Mockito.when(mockableBean1.greet("Stuart")).thenReturn("A mock for Stuart"); (2)
}
@Test
public void firstTest() {
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals(null, mockableBean2.greet("Stuart")); (3)
}
@Test
public void secondTest() {
Mockito.when(mockableBean2.greet("Stuart")).thenReturn("Bonjour Stuart"); (4)
Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
}
@ApplicationScoped
public static class MockableBean1 {
public String greet(String name) {
return "Hello " + name;
}
}
@ApplicationScoped
public static class MockableBean2 {
public String greet(String name) {
return "Hello " + name;
}
}
}
1 | @InjectMock 会创建一个 Mockito mock,该 mock 可在测试类的测试方法中使用(其他测试类不受此影响)。 |
2 | mockableBean1 在此处为类的所有测试方法配置。 |
3 | 由于 mockableBean2 mock 未配置,它将返回默认的 Mockito 响应。 |
4 | 在此测试中,mockableBean2 已配置,因此它返回配置的响应。 |
虽然上面的测试很好地展示了 @InjectMock
的功能,但它并不是实际测试的良好表示。在实际测试中,我们很可能会配置一个 mock,然后测试使用被 mock bean 的 bean。下面是一个示例:
@QuarkusTest
public class MockGreetingServiceTest {
@InjectMock
GreetingService greetingService;
@Test
public void testGreeting() {
when(greetingService.greet()).thenReturn("hi");
given()
.when().get("/greeting")
.then()
.statusCode(200)
.body(is("hi")); (1)
}
@Path("greeting")
public static class GreetingResource {
final GreetingService greetingService;
public GreetingResource(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GET
@Produces("text/plain")
public String greet() {
return greetingService.greet();
}
}
@ApplicationScoped
public static class GreetingService {
public String greet(){
return "hello";
}
}
}
1 | 由于我们将 greetingService 配置为 mock,因此使用 GreetingService bean 的 GreetingResource 将获得被 mock 的响应,而不是常规 GreetingService bean 的响应。 |
默认情况下,@InjectMock
注释可用于任何正常范围的 CDI bean(例如 @ApplicationScoped
、@RequestScoped
)。通过添加 @MockitoConfig(convertScopes = true)
注释,可以 mock @Singleton
bean。这将把 @Singleton
bean 转换为 @ApplicationScoped
bean 以用于测试。
这被认为是一个高级选项,只有在您完全了解更改 bean 范围的后果时才应执行。
12.2.2. 使用 Spy 而不是 Mock,并使用 @InjectSpy
基于 InjectMock
提供的功能,Quarkus 还允许用户轻松利用 Mockito 来 spy QuarkusMock
支持的 bean。此功能通过 @io.quarkus.test.junit.mockito.InjectSpy
注释提供,该注释在 quarkus-junit5-mockito
依赖项中可用。
有时在测试中,您只需要验证某个逻辑路径是否被执行,或者只需要 stub out 单个方法的响应,同时仍然执行 Spy 克隆上其余方法的执行。有关 Spy 部分 mock 的更多详细信息,请参阅 Mockito 文档 - Spying on real objects。在这些情况中的任何一种情况下,Spy 对象都是首选。使用 @InjectSpy
,前面的示例可以这样编写:
@QuarkusTest
public class SpyGreetingServiceTest {
@InjectSpy
GreetingService greetingService;
@Test
public void testDefaultGreeting() {
given()
.when().get("/greeting")
.then()
.statusCode(200)
.body(is("hello"));
Mockito.verify(greetingService, Mockito.times(1)).greet(); (1)
}
@Test
public void testOverrideGreeting() {
doReturn("hi").when(greetingService).greet(); (2)
given()
.when().get("/greeting")
.then()
.statusCode(200)
.body(is("hi")); (3)
}
@Path("greeting")
public static class GreetingResource {
final GreetingService greetingService;
public GreetingResource(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GET
@Produces("text/plain")
public String greet() {
return greetingService.greet();
}
}
@ApplicationScoped
public static class GreetingService {
public String greet(){
return "hello";
}
}
}
1 | 我们不想覆盖值,我们只想确保 GreetingService 上的 greet 方法在此测试中被调用。 |
2 | 在这里,我们告诉 Spy 返回“hi”而不是“hello”。当 GreetingResource 请求 GreetingService 的问候时,我们得到被 mock 的响应,而不是常规 GreetingService bean 的响应。有时使用 when(Object) stubbing spy 是不可能或不切实际的。因此,在使用 spy 时,请考虑使用 doReturn|Answer|Throw() 系列方法进行 stubbing。 |
3 | 我们正在验证我们是否从 Spy 获得被 mock 的响应。 |
12.2.3. 将 @InjectMock
与 @RestClient
结合使用
@RegisterRestClient
在运行时注册 REST Client 的实现,并且由于 bean 需要是常规范围,因此您必须使用 @ApplicationScoped
对接口进行注释。
@Path("/")
@ApplicationScoped
@RegisterRestClient
public interface GreetingService {
@GET
@Path("/hello")
@Produces(MediaType.TEXT_PLAIN)
String hello();
}
对于测试类,这是一个示例:
@QuarkusTest
public class GreetingResourceTest {
@InjectMock
@RestClient (1)
GreetingService greetingService;
@Test
public void testHelloEndpoint() {
Mockito.when(greetingService.hello()).thenReturn("hello from mockito");
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("hello from mockito"));
}
}
1 | 指示此注入点用于 RestClient 的实例。 |
12.3. 使用 Panache 进行 Mock
如果您正在使用 quarkus-hibernate-orm-panache
或 quarkus-mongodb-panache
扩展,请查看 Hibernate ORM with Panache Mocking 和 MongoDB with Panache Mocking 文档,了解 mock 数据访问的最简单方法。
13. 测试安全
如果您正在使用 Quarkus Security,请查看 测试安全 部分,了解如何轻松测试应用程序的安全功能。
14. 在 Quarkus 应用程序启动之前启动服务
在 Quarkus 应用程序启动进行测试之前,启动 Quarkus 应用程序所依赖的某些服务是一项非常常见的需求。为了满足这一需求,Quarkus 提供了 @io.quarkus.test.common.QuarkusTestResource
和 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
。
只需用 @QuarkusTestResource
注释测试套件中的任何测试,Quarkus 将在运行任何测试之前运行相应的 QuarkusTestResourceLifecycleManager
。测试套件也可以自由使用多个 @QuarkusTestResource
注释,在这种情况下,所有相应的 QuarkusTestResourceLifecycleManager
对象将在测试运行之前运行。
测试资源是全局的,即使它们是在测试类或自定义配置文件上定义的,这意味着它们将对所有测试激活,即使我们删除了重复项。如果您只想在单个测试类或测试配置文件上启用测试资源,您可以使用 @QuarkusTestResource(restrictToAnnotatedClass = true) 。 |
当使用多个测试资源时,它们可以并行启动。为此,您需要设置 @QuarkusTestResource(parallel = true) 。 |
Quarkus 开箱即用地提供了 QuarkusTestResourceLifecycleManager
的一些实现(请参阅 io.quarkus.test.h2.H2DatabaseTestResource
,它启动一个 H2 数据库,或 io.quarkus.test.kubernetes.client.KubernetesServerTestResource
,它启动一个 mock Kubernetes API 服务器),但创建自定义实现以满足特定应用程序需求也很常见。常见情况包括使用 Testcontainers 启动 docker 容器(示例可在此处找到),或者使用 Wiremock 启动 mock HTTP 服务器(示例可在此处找到)。
由于 QuarkusTestResourceLifecycleManager 不是 CDI Bean,因此实现它的类不能用 @Inject 注入字段。您可以使用 String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class); 。 |
14.1. 修改测试类
在创建自定义 QuarkusTestResourceLifecycleManager
以需要将某些内容注入测试类时,可以使用 inject
方法。例如,如果您有一个如下所示的测试:
@QuarkusTest
@QuarkusTestResource(MyWireMockResource.class)
public class MyTest {
@InjectWireMock // this a custom annotation you are defining in your own application
WireMockServer wireMockServer;
@Test
public someTest() {
// control wiremock in some way and perform test
}
}
通过将 MyWireMockResource
注入 wireMockServer
字段,可以在以下代码片段的 inject
方法中完成:
public class MyWireMockResource implements QuarkusTestResourceLifecycleManager {
WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer(8090);
wireMockServer.start();
// create some stubs
return Map.of("some.service.url", "localhost:" + wireMockServer.port());
}
@Override
public synchronized void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
wireMockServer = null;
}
}
@Override
public void inject(TestInjector testInjector) {
testInjector.injectIntoFields(wireMockServer, new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
}
}
值得一提的是,这种注入到测试类的过程不受 CDI 控制,而是在 CDI 完成对测试类的任何必要注入之后进行的。 |
14.2. 基于注释的测试资源
可以编写使用注释启用和配置的测试资源。这通过将 @QuarkusTestResource
放在将用于启用和配置测试资源的注释上来实现。
例如,这定义了 @WithKubernetesTestServer
注释,您可以使用它在测试类上激活 KubernetesServerTestResource
,但仅限于注释的测试类。您还可以将它们放在 QuarkusTestProfile
测试配置文件上。
@QuarkusTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithKubernetesTestServer {
/**
* Start it with HTTPS
*/
boolean https() default false;
/**
* Start it in CRUD mode
*/
boolean crud() default true;
/**
* Port to use, defaults to any available port
*/
int port() default 0;
}
KubernetesServerTestResource
类必须实现 QuarkusTestResourceConfigurableLifecycleManager
接口,才能使用前面的注释进行配置。
public class KubernetesServerTestResource
implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> {
private boolean https = false;
private boolean crud = true;
private int port = 0;
@Override
public void init(WithKubernetesTestServer annotation) {
this.https = annotation.https();
this.crud = annotation.crud();
this.port = annotation.port();
}
// ...
}
如果您想使注释可重复,则包含的注释类型必须用 @QuarkusTestResourceRepeatable
进行注释。例如,这将定义一个可重复的 @WithRepeatableTestResource
注释。
@QuarkusTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(WithRepeatableTestResource.List.class)
public @interface WithRepeatableTestResource {
String key() default "";
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@QuarkusTestResourceRepeatable(WithRepeatableTestResource.class)
@interface List {
WithRepeatableTestResource[] value();
}
}
14.3. @WithTestResource
的用法
虽然 @QuarkusTestResource
提供的测试资源是全局可用的,或者限制在注释的测试类(restrictToAnnotatedClass
)中,但 @WithTestResource
注释允许通过测试资源进一步对测试进行分组以进行执行。@WithTestResource
具有一个 scope
属性,该属性接受一个 TestResourceScope
枚举值。
-
TestResourceScope.MATCHING_RESOURCES
(默认):Quarkus 将具有相同测试资源的测试分组并一起运行。执行完一个组后,所有测试资源都将停止,然后执行下一组。 -
TestResourceScope.RESTRICTED_TO_CLASS
:测试资源仅适用于注释的测试类,并在测试类执行后停止。 -
TestResourceScope.GLOBAL
:测试资源适用于测试套件中的所有测试。
Quarkus 需要重启,如果以下任一情况为真:
-
当前测试的至少一个测试资源限制在测试类。
-
下一个测试的至少一个测试资源限制在测试类。
-
正在使用不同的
MATCHING_RESOURCES
范围的测试资源。
15. 挂起检测
@QuarkusTest
支持挂起检测,以帮助诊断任何意外的挂起。如果在指定时间内没有取得任何进展(即没有调用 JUnit 回调),Quarkus 将在控制台打印堆栈跟踪以帮助诊断挂起。此超时时间的默认值为 10 分钟。
将不采取进一步行动,测试将正常继续(通常直到 CI 超时),但打印的堆栈跟踪应有助于诊断构建失败的原因。您可以使用 quarkus.test.hang-detection-timeout
系统属性来控制此超时(您也可以在 application.properties 中设置它,但 Quarkus 启动之前不会读取它,因此 Quarkus 启动的超时时间将是默认的 10 分钟)。
16. 原生可执行文件测试
还可以使用 @QuarkusIntegrationTest
测试原生可执行文件。这支持本指南中提到的所有功能,但注入测试除外(原生可执行文件在单独的非 JVM 进程中运行,因此这实际上是不可能的)。
这在《构建原生映像指南》中已涵盖。
17. 使用 @QuarkusIntegrationTest
@QuarkusIntegrationTest
应用于启动和测试 Quarkus 构建生成的工件,并支持测试 jar(任何类型)、原生映像或容器映像。简而言之,这意味着如果 Quarkus 构建的结果(mvn package
或 gradle build
)是一个 jar,那么该 jar 将作为 java -jar …
启动,并针对它运行测试。如果相反构建了原生映像,那么应用程序将作为 ./application …
启动,同样,测试也是针对正在运行的应用程序运行的。最后,如果在构建过程中创建了容器映像(通过包含 quarkus-container-image-jib
、quarkus-container-image-docker
或 container-image-podman
扩展并配置了 quarkus.container-image.build=true
属性),那么将创建并运行一个容器(这需要 docker
或 podman
可执行文件)。
这是一个黑盒测试,支持相同的功能集并具有相同的限制。
由于用 |
pom.xml
文件包含:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
这指示 failsafe-maven-plugin 运行 integration-test。
然后,打开 src/test/java/org/acme/quickstart/GreetingResourceIT.java
。它包含:
package org.acme.quickstart;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest (1)
public class GreetingResourceIT extends GreetingResourceTest { (2)
// Run the same tests
}
1 | 使用另一个测试运行程序,该程序在测试之前从原生文件启动应用程序。可执行文件由Failsafe Maven Plugin检索。 |
2 | 为了方便起见,我们扩展了之前的测试,但您也可以实现自己的测试。 |
更多信息可以在《测试原生可执行文件指南》中找到。
当使用 |
虽然使用 |
17.1. 启动容器
当 @QuarkusIntegrationTest
导致启动容器时(因为应用程序已使用 quarkus.container-image.build
设置为 true
构建),容器将在可预测的容器网络上启动。这有助于编写需要启动服务来支持应用程序的集成测试。这意味着 @QuarkusIntegrationTest
与通过Dev Services启动的容器开箱即用,但也意味着它允许使用启动附加容器的 QuarkusTestLifecycleManager 资源。这可以通过让您的 QuarkusTestLifecycleManager
实现 io.quarkus.test.common.DevServicesContext.ContextAware
来实现。一个简单的例子可能如下:
运行要测试的资源(例如,通过 Testcontainers 的 PostgreSQL)的容器从容器的网络中分配了一个 IP 地址。使用容器网络上的容器“公共”IP 和“未映射”端口号来连接服务。Testcontainers 库通常返回不考虑容器网络的连接字符串,因此需要额外的代码来使用容器网络上的容器 IP 和未映射的端口号为 Quarkus 提供“正确的”连接字符串。
以下示例说明了与 PostgreSQL 的用法,但该方法适用于所有容器。
import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class CustomResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {
private Optional<String> containerNetworkId;
private JdbcDatabaseContainer container;
@Override
public void setIntegrationTestContext(DevServicesContext context) {
containerNetworkId = context.containerNetworkId();
}
@Override
public Map<String, String> start() {
// start a container making sure to call withNetworkMode() with the value of containerNetworkId if present
container = new PostgreSQLContainer<>("postgres:latest").withLogConsumer(outputFrame -> {});
// apply the network to the container
containerNetworkId.ifPresent(container::withNetworkMode);
// start container before retrieving its URL or other properties
container.start();
String jdbcUrl = container.getJdbcUrl();
if (containerNetworkId.isPresent()) {
// Replace hostname + port in the provided JDBC URL with the hostname of the Docker container
// running PostgreSQL and the listening port.
jdbcUrl = fixJdbcUrl(jdbcUrl);
}
// return a map containing the configuration the application needs to use the service
return ImmutableMap.of(
"quarkus.datasource.username", container.getUsername(),
"quarkus.datasource.password", container.getPassword(),
"quarkus.datasource.jdbc.url", jdbcUrl);
}
private String fixJdbcUrl(String jdbcUrl) {
// Part of the JDBC URL to replace
String hostPort = container.getHost() + ':' + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT);
// Host/IP on the container network plus the unmapped port
String networkHostPort =
container.getCurrentContainerInfo().getConfig().getHostName()
+ ':'
+ PostgreSQLContainer.POSTGRESQL_PORT;
return jdbcUrl.replace(hostPort, networkHostPort);
}
@Override
public void stop() {
// close container
}
}
CustomResource
将使用 @QuarkusTestResource
在 @QuarkusIntegrationTest
上激活,这已在本指南的相应部分进行了说明。
17.2. 对正在运行的应用程序执行
@QuarkusIntegrationTest
支持针对已正在运行的应用程序实例执行测试。这可以通过在运行测试时设置 quarkus.http.test-host
系统属性来实现。
这的一种用途可以是以下 Maven 命令,它强制 @QuarkusIntegrationTest
针对可访问的 http://1.2.3.4:4321
执行。
./mvnw verify -Dquarkus.http.test-host=1.2.3.4 -Dquarkus.http.test-port=4321
要测试仅接受 SSL/TLS 连接的正在运行的实例(例如:https://1.2.3.4:4321
),请将系统属性 quarkus.http.test-ssl-enabled
设置为 true
,并将 quarkus.http.test-ssl-port
设置为目标 HTTPS 端口。
18. 将 @QuarkusTest
与其他类型的测试混合
在单个执行运行(例如,在单个 Maven Surefire 插件执行中)中,不允许将用 @QuarkusTest
注释的测试与用 @QuarkusDevModeTest
、@QuarkusProdModeTest
或 @QuarkusUnitTest
注释的测试混合,而最后三个可以共存。
此限制的原因是 @QuarkusTest
为整个测试执行运行生命周期启动 Quarkus 服务器,从而阻止其他测试启动自己的 Quarkus 服务器。
为了减轻此限制,@QuarkusTest
注释定义了一个 JUnit 5 @Tag
:io.quarkus.test.junit.QuarkusTest
。您可以使用此标签在特定的执行运行中隔离 @QuarkusTest
测试,例如使用 Maven Surefire 插件:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<id>default-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<excludedGroups>io.quarkus.test.junit.QuarkusTest</excludedGroups>
</configuration>
</execution>
<execution>
<id>quarkus-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<groups>io.quarkus.test.junit.QuarkusTest</groups>
</configuration>
</execution>
</executions>
<configuration>
<systemProperties>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemProperties>
</configuration>
</plugin>
目前 对于 Maven,这意味着前者应由 surefire 插件运行,而后者应由 failsafe 插件运行。 对于 Gradle,这意味着这两种类型的测试应属于不同的源集。 源集配置示例
|
19. 从 IDE 运行 @QuarkusTest
大多数 IDE 都提供直接将选定类作为 JUnit 测试运行的可能性。为此,您应该在所选 IDE 的设置中设置一些属性:
19.1. Eclipse 独立 JRE 定义
将当前的“已安装 JRE”定义复制到一个新的定义中,您将在其中将属性添加为新的 VM 参数:
-
-Djava.util.logging.manager=org.jboss.logmanager.LogManager
-
-Dmaven.home=<path-to-your-maven-installation>
将此 JRE 定义用作 Quarkus 项目的目标运行时,并且该解决方法将应用于任何“Run as JUnit”配置。
20. 测试 Dev Services
默认情况下,测试应能与Dev Services正常工作,但出于某些用例的考虑,您可能需要在测试中访问自动配置的属性。
您可以通过 io.quarkus.test.common.DevServicesContext
来实现此目的,该对象可以直接注入到任何 @QuarkusTest
或 @QuarkusIntegrationTest
中。您所要做的就是定义一个 DevServicesContext
类型的字段,它将被自动注入。使用此字段,您可以检索已设置的任何属性。通常,这用于直接从测试本身连接到资源,例如连接到 kafka 以向正在测试的应用程序发送消息。
注入也支持到实现 io.quarkus.test.common.DevServicesContext.ContextAware
的对象中。如果您有一个实现 io.quarkus.test.common.DevServicesContext.ContextAware
的字段,Quarkus 将调用 setIntegrationTestContext
方法将上下文传递给该对象。这允许将客户端逻辑封装在实用类中。
QuarkusTestResourceLifecycleManager
实现也可以实现 ContextAware
以访问这些属性,这允许您在 Quarkus 启动之前设置资源(例如,配置 KeyCloak 实例,向数据库添加数据等)。
对于导致将应用程序作为容器启动的 |
21. 测试组件
Quarkus 提供了 QuarkusComponentTestExtension
,这是一个 JUnit 扩展,用于简化组件的测试以及其依赖项的 Mock。此 JUnit 扩展在 quarkus-junit5-component
依赖项中可用。
假设我们有一个组件 Foo
- 一个带有两个注入点的 CDI bean。
Foo
组件package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped (1)
public class Foo {
@Inject
Charlie charlie; (2)
@ConfigProperty(name = "bar")
boolean bar; (3)
public String ping() {
return bar ? charlie.ping() : "nok";
}
}
1 | Foo 是一个 @ApplicationScoped CDI bean。 |
2 | Foo 依赖于 Charlie ,后者声明了一个方法 ping() 。 |
3 | Foo 依赖于配置属性 bar 。@Inject 不需要用于此注入点,因为它还声明了一个 CDI 限定符 - 这是 Quarkus 特有的功能。 |
那么组件测试可能看起来像:
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest (1)
@TestConfigProperty(key = "bar", value = "true") (2)
public class FooTest {
@Inject
Foo foo; (3)
@InjectMock
Charlie charlieMock; (4)
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK"); (5)
assertEquals("OK", foo.ping());
}
}
1 | QuarkusComponentTest 注释注册了 JUnit 扩展。 |
2 | 为测试设置配置属性。 |
3 | 测试注入被测试的组件。用 @Inject 注释的所有字段的类型被视为被测试的组件类型。您还可以通过 @QuarkusComponentTest#value() 指定其他组件类。此外,在测试类上声明的静态嵌套类也是组件。 |
4 | 测试还注入了 Charlie 的 mock。Charlie 是一个未满足的依赖项,为该依赖项自动注册了一个合成的 @Singleton bean。注入的引用是一个“未配置”的 Mockito mock。 |
5 | 我们可以在测试方法中利用 Mockito API 来配置行为。 |
您可以在《测试组件参考指南》中找到更多示例和提示。