Testing Your Application
了解如何测试 Quarkus 应用程序。本指南涵盖:
-
Testing in JVM mode
-
Testing in native mode
-
向测试中注入资源
- Prerequisites
- Architecture
- Solution
- Recap of HTTP based Testing in JVM mode
- Testing a specific endpoint
- Injection into tests
- Applying Interceptors to Tests
- Tests and Transactions
- Enrichment via QuarkusTest*Callback
- Testing Different Profiles
- Mock Support
- Testing Security
- Starting services before the Quarkus application starts
- Hang Detection
- Native Executable Testing
- Using
@QuarkusIntegrationTest
- Mixing
@QuarkusTest
with other type of tests - Running
@QuarkusTest
from an IDE - Testing Dev Services
- Testing Components
Prerequisites
include::./_includes/prerequisites.adoc[]* Getting Started Guide的完成问候应用程序
Architecture
在本指南中,我们扩展了入门指南中创建的初始测试。我们涵盖了向测试中注入以及如何测试原生可执行文件。
Quarkus 支持持续测试,但这由 Continuous Testing Guide涵盖。 |
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
该解决方案位于 getting-started-testing
directory。
本指南假设您已经有了 `getting-started`目录中完成的应用程序。
Recap of HTTP based Testing in JVM mode
如果您已经从入门示例开始,您应该已经有一个完成的测试,包括正确的工具设置。
在您的构建文件中,您应该看到 2 个测试依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
dependencies {
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
}
`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 端点。在运行测试之前,应用程序将在测试运行之前启动。
Controlling the test port
虽然 Quarkus 默认侦听端口 8080
,但它在运行测试时会默认使用 8081
。这样您就可以同时运行测试,同时应用程序也在并行运行。
Changing the test port
您可以通过在
|
Quarkus 还提供了 RestAssured 集成,可以在运行测试之前更新 RestAssured 使用的默认端口,因此无需额外的配置。
Controlling HTTP interaction timeout
在测试中使用 REST Assured 时,连接和响应超时时间设置为 30 秒。您可以使用 quarkus.http.test-timeout
属性覆盖此设置:
quarkus.http.test-timeout=10s
Injecting a 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
允许您注入 URI
、URL
和 String
的 URL 表示。
Testing a specific endpoint
RESTassured 和 @TestHTTPResource
允许您指定要测试的端点类,而不是对路径进行硬编码。它目前支持 Jakarta REST 端点、Servlet 和 Reactive Routes。这使得查看给定测试正在测试的确切端点变得更加容易。
为了这些示例的目的,我将假设我们有一个类似于以下内容的端点:
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
当前不支持使用 |
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 结束。 |
RESTassured
要控制 RESTassured 基本路径(即作为每个请求的根的默认路径),可以使用 io.quarkus.test.common.http.TestHTTPEndpoint
注解。这可以应用于类或方法级别。要测试问候资源,我们可以执行以下操作:
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 是此测试的默认路径 |
Injection into tests
到目前为止,我们仅介绍了通过 HTTP 端点测试应用程序的集成式测试,但如果我们希望进行单元测试并直接测试我们的 bean 该怎么办?
Quarkus 支持这一点,允许您通过 @Inject
注释将 CDI bean 注入测试(实际上,Quarkus 中的测试就是完整的 CDI bean,因此您可以使用所有 CDI 功能)。我们创建一个无需使用 HTTP 即可直接测试 GreetingService 的简单测试:
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 将被注入测试中 |
Applying Interceptors to Tests
如上文所述,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());
}
}
Tests and Transactions
您可以在测试中使用标准 Quarkus @Transactional
注释,但这意味着您的测试对数据库所做的更改将是持久的。如果您希望在测试结束时将进行的任何更改回滚,则可以使用 io.quarkus.test.TestTransaction
注释。这将在事务中运行测试方法,但在测试方法完成后将其回滚,以还原任何数据库更改。
Enrichment via QuarkusTest*Callback
或者除了拦截器之外,您还可以通过实现以下回调接口来丰富 all 您的 @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
可以从测试类或方法中读取注释来控制回调将执行的操作。 |
虽然可以使用 JUnit Jupiter 回调接口(例如 BeforeEachCallback
),但您可能会遇到类加载问题,因为 Quarkus 必须在 JUnit 不了解的自定义类加载器中运行测试。
Testing Different Profiles
到目前为止,在所有示例中,我们只对所有测试启动一次 Quarkus。在运行第一个测试之前,Quarkus 将启动,然后所有测试将运行,最后 Quarkus 将关闭。这会形成非常快速的测试体验,但它会受到一点限制,因为您无法测试不同的配置。
为了解决这个问题,Quarkus 支持测试配置文件。如果某个测试具有与先前运行的测试不同的配置文件,则在运行测试之前,Quarkus 将关闭并使用新配置文件启动。这显然有点慢,因为它在测试时间中增加了一个关闭/启动周期,但它提供了很大的灵活性。
为了减少 Quarkus 需要重新启动的次数,io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer
被注册为一个全局 ClassOrderer
,如 JUnit 5 User Guide 中所述。此 ClassOrderer
的行为可通过 junit-platform.properties
进行配置(请参阅源代码或 javadoc 以了解更多详细信息)。还可以通过设置另一个由 JUnit 5 提供的 ClassOrderer
甚至设置您自己的自定义 ClassOrderer
来完全禁用它。请注意,自 JUnit 5.8.2 起, only a single junit-platform.properties
is picked up and a warning is logged if more than one is found 。如果您遇到此类警告,可以通过排除从类路径中移除 Quarkus 提供的 junit-platform.properties
来消除它们:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-properties</artifactId>
</exclusion>
</exclusions>
</dependency>
Writing a Profile
要实现测试配置文件,我们需要实现 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.WithTestResource} 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)`注释测试类来实现此目的。
所有测试配置文件配置存储在一个类中,这使得我们可以轻松判断上一项测试是否使用相同的配置运行。
Running specific tests
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
是唯一一个使用其tags
方法与quarkus.test.profile.tags
的值相匹配的QuarkusTestProfile
实现。
Mock Support
Quarkus 支持使用两种不同的方法来使用模拟对象,你可以使用 CDI 替代来模拟所有测试类的 bean,也可以使用 QuarkusMock
来逐个测试模拟 bean。
CDI @Alternative
mechanism.
要使用此功能,只需使用 src/test/java
目录中的类来覆盖你想要模拟的 bean,并在 bean 上放置 @Alternative
和 @Priority(1)
注释,或者,可以使用一个方便的 io.quarkus.test.Mock
抽象注释,内置的此抽象注释声明 @Alternative
、@Priority(1)
和 @Dependent
,例如,如果我有以下服务:
@ApplicationScoped
public class ExternalService {
public String service() {
return "external";
}
}
我可以在 src/test/java
中使用以下类对其进行模拟:
@Mock
@ApplicationScoped (1)
public class MockExternalService extends ExternalService {
@Override
public String service() {
return "mock";
}
}
1 | 覆盖在 @Mock 抽象注释中声明的 @Dependent 作用域。 |
替代项出现在 src/test/java
目录而不是 src/main/java
中非常重要,因为否则它将始终生效,而不仅仅是在测试时。
请注意,目前此方法不适用于本机图像测试,因为这需要将测试备选方案编译到本机图像中。
Mocking using QuarkusMock
`io.quarkus.test.junit.QuarkusMock`类可以用来临时模拟任何普通作用域的 bean。如果你在 `@BeforeAll`方法中使用此方法,模拟将对当前类的所有测试生效,如果你在测试方法中使用此方法,则模拟将仅对当前测试的持续时间有效。
此方法可用于任何正常的范围 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 ,此模拟用于两种测试方法 |
2 | 我们使用 `installMockForInstance`替换注入的 bean,这对测试方法的持续时间有效。 |
请注意,不依赖 Mockito,你可以使用任何你喜欢的模拟库,甚至可以手动重写对象以提供所需的行为。
使用 |
Further simplification with @InjectMock
在 `QuarkusMock`提供的功能的基础上,Quarkus 还允许用户毫不费力地利用 Mockito来模拟 `QuarkusMock`支持的 bean。
此功能可用于带有 `@io.quarkus.test.InjectMock`注释 only if 的 `quarkus-junit5-mockito`依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
使用 @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 模拟,然后在测试类的测试方法中使用该模拟(其他测试类不会受到 *not*的影响) |
2 | `mockableBean1`在此处为该类的每个测试方法进行配置 |
3 | 由于未配置 `mockableBean2`模拟,因此它将返回默认的 Mockito 响应。 |
4 | 在此测试中,`mockableBean2`已配置,因此它返回配置的响应。 |
尽管上面的测试非常适合展示 `@InjectMock`的能力,但它并不能很好地代表一个真实的测试。在真实的测试中,我们很可能会配置一个模拟,然后测试一个使用模拟 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`配置为模拟,使用 `GreetingService`bean 的 `GreetingResource ,因此我们得到的不是常规 `GreetingService`bean 的响应,而是模拟的响应 |
默认情况下,@InjectMock`注释可用于任何正常的 CDI 范围 bean(例如 `@ApplicationScoped
,@RequestScoped
)。可以通过添加 `@MockitoConfig(convertScopes = true)`注释来模拟 `@Singleton`bean。这将为测试将 `@Singleton`bean 转换为 `@ApplicationScoped`bean。
这被认为是一个高级选项,只有在您完全理解变更 bean 作用域的后果时才应该执行此操作。
Using Spies instead of Mocks with @InjectSpy
建立在 InjectMock
提供的功能之上,Quarkus 还允许用户毫不费力地利用 Mockito 监视受 QuarkusMock
支持的 bean。此功能通过 @io.quarkus.test.junit.mockito.InjectSpy
注解提供,该注解在 quarkus-junit5-mockito
依赖项中提供。
有时在测试中,您只需要验证采用了某个逻辑路径,或者只需要截取单个方法的响应,同时仍然执行 Spied 克隆上的其他方法。更多有关 Spy 部分模拟的详细信息,请参阅 Mockito documentation - 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 中请求问候时,我们得到了模拟的响应,而不是常规 GreetingService bean 的响应。有时使用 when(Object) 截取模拟是不可能的或不切实际的。因此,在使用模拟时,请考虑使用 doReturn|Answer|Throw() 系列方法进行截取。 |
3 | 我们正在验证是否从 Spy 得到模拟的响应。 |
Using @InjectMock
with @RestClient
@RegisterRestClient
在运行时注册 REST 客户端的实现,并且由于 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 的实例。 |
Mocking with Panache
如果您正在使用 quarkus-hibernate-orm-panache
或 quarkus-mongodb-panache
扩展,请查看 Hibernate ORM with Panache Mocking 和 MongoDB with Panache Mocking 文档,了解模拟数据访问的最简单方法。
Testing Security
如果您正在使用 Quarkus Security,请查看 Testing Security 部分,了解如何轻松测试应用程序的安全功能的信息。
Starting services before the Quarkus application starts
在测试 Quarkus 应用程序启动之前启动 Quarkus 应用程序所依赖的一些服务是一个非常常见的需求。为了满足此需求,Quarkus 提供了 @io.quarkus.test.common.WithTestResource
和 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
。
当使用 @WithTestResource
注解的测试时,Quarkus 将在测试之前运行相应的 QuarkusTestResourceLifecycleManager
。
默认情况下,@WithTestResource
仅适用于对其放置注解的测试。使用 @WithTestResource
注解的每个测试都将导致重新扩充和重新启动应用程序(类似于在开发模式中检测到更改时发生的情况),以便纳入由注解配置的设置。这意味着如果在整个测试套件中使用了大量此注解,这些重新启动将会影响测试执行速度。
测试资源将用于给定的测试类或自定义配置文件。要激活所有测试,可以使用 |
当使用多个测试资源时,可以同时启动它们。为此,您需要设置 |
Quarkus 提供了一些开箱即用的 QuarkusTestResourceLifecycleManager
实现(请参见启动 H2 数据库的 io.quarkus.test.h2.H2DatabaseTestResource
或启动模拟 Kubernetes API 服务器的 io.quarkus.test.kubernetes.client.KubernetesServerTestResource
),但通常会创建自定义实现来满足特定应用程序需求。常见的情况包括使用 Testcontainers 启动 Docker 容器(可以在 here 中找到示例),或使用 Wiremock 启动模拟 HTTP 服务器(可以在 here 中找到示例)。
由于 |
Altering the test class
在创建需要将一些内容注入测试类的自定义 QuarkusTestResourceLifecycleManager
时,可以使用 inject
方法。例如,如果您有一个类似于以下内容的测试:
@QuarkusTest
@WithTestResource(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
}
}
如以下代码片段的`inject`方法所示,可以通过让`MyWireMockResource`注入`wireMockServer`字段来完成:
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 对测试类执行任何必要的注入之后才会发生。
Annotation-based test resources
可以使用注释启用并配置测试资源。这是通过在注释上放置`@WithTestResource`来实现的,该注释将用于启用并配置测试资源。
例如,这定义了 @WithKubernetesTestServer`注释,您可以在测试中使用它来激活`KubernetesServerTestResource
,但仅适用于带注释的测试类。您还可以将它们放在`QuarkusTestProfile`测试配置文件中。
@WithTestResource(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();
}
// ...
}
如果您要使注释可重复,则必须使用`@WithTestResourceRepeatable`对包含的注释类型进行注释。例如,这将定义一个可重复的`@WithRepeatableTestResource`注释。
@WithTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(WithRepeatableTestResource.List.class)
public @interface WithRepeatableTestResource {
String key() default "";
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@WithTestResourceRepeatable(WithRepeatableTestResource.class)
@interface List {
WithRepeatableTestResource[] value();
}
}
Hang Detection
`@QuarkusTest`支持挂起检测,以帮助诊断任何意外挂起。如果在指定时间内没有取得任何进展(即未调用任何 JUnit 回调),则 Quarkus 将向控制台打印堆栈跟踪以帮助诊断挂起。此超时的默认值为 10 分钟。
不会执行进一步的操作,测试将正常继续(通常持续到 CI 超时),但是打印的堆栈跟踪应该有助于诊断构建失败的原因。您可以使用`quarkus.test.hang-detection-timeout`系统属性控制此超时(您也可以在 application.properties 中设置此属性,但这在 Quarkus 启动之前不会被读取,因此 Quarkus 启动的超时将默认为 10 分钟)。
Native Executable Testing
也可以使用`@QuarkusIntegrationTest`测试本机可执行文件。这支持本指南中提到的所有功能,除了注入测试(并且本机可执行文件在单独的非 JVM 进程中运行,这实际上是不可能的)。
这在Native Executable Guide中有所涉及。
Using @QuarkusIntegrationTest
@QuarkusIntegrationTest
应该用于启动和测试 Quarkus 构建产生的工件,并且支持测试 jar(任何类型的)、本机映像或容器映像。简单来说,这意味着如果 Quarkus 构建 ({@s23} 或 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 | 我们扩展了之前的测试以提供便利,但您也可以执行自己的测试。 |
可以在 Testing the native executable Guide中找到更多信息。
使用 @QuarkusIntegrationTest
进行应用程序测试时,将使用 prod
配置文件启动该应用程序,不过,可以通过使用 quarkus.test.integration-test-profile
属性更改此设置。
在单元测试中 src/test/resources/application.properties
允许添加针对测试特定配置属性(注意此处是 test
,而不是 main
),但在集成测试中不允许此操作。
Launching containers
当 @QuarkusIntegrationTest
导致启动容器(因为在构建应用程序时将 quarkus.container-image.build
设置为 true
),该容器将在可预测的容器网络中启动。这能简化编写需要启动服务支持应用程序的集成测试。这表示 @QuarkusIntegrationTest
完全适用于通过 Dev Services 启动的容器,但这也意味着它允许使用 QuarkusTestLifecycleManager 资源,这些资源可启动附加容器。可以通过让 QuarkusTestLifecycleManager
实现 io.quarkus.test.common.DevServicesContext.ContextAware
来实现这一点。一个简单的示例可能是以下示例:
运行需要测试的资源的容器(例如通过 Testcontainers 的 PostgreSQL)将从容器的网络中分配一个 IP 地址。使用容器网络中的容器“公用”IP 以及“未映射”端口号连接到该服务。Testcontainers 库通常返回不符合容器网络的连接字符串,因此需要附加代码,以便通过容器网络上的容器 IP 和 unmapped 端口号向 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
将使用 @WithTestResource
在 @QuarkusIntegrationTest
中激活,如本文档的相应部分中所述。
Executing against a running application
@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
。
Mixing @QuarkusTest
with other type of tests
在单次执行运行(例如单次 Maven Surefire Plugin 执行)中,不允许将标有 @QuarkusTest
的测试与标有 @QuarkusDevModeTest
、 @QuarkusProdModeTest
或 @QuarkusUnitTest
的测试混合使用,而后三者可以共存。
此限制的原因是, @QuarkusTest
会在测试执行运行的整个生命周期内启动 Quarkus 服务器,从而阻止其他测试启动自己的 Quarkus 服务器。
为消除此限制, @QuarkusTest
注释定义了一个 JUnit 5 @Tag
: io.quarkus.test.junit.QuarkusTest
。您可以使用此标签在特定执行运行中隔离 @QuarkusTest
测试,例如使用 Maven Surefire Plugin:
<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>
Running @QuarkusTest
from an IDE
大多数 IDE 都可直接作为 JUnit 测试运行所选类。为此,应在所选 IDE 设置中设置一些属性:
-
java.util.logging.manager
(see Logging Guide) -
maven.home
(仅在${maven.home}/conf/settings.xml
中存在任何自定义设置时,请参见 Maven Guide) -
maven.settings
(如果测试应使用自定义版本的settings.xml
文件)
Eclipse separate JRE definition
将当前的“已安装 JRE”定义复制到新的定义中,您将在其中添加属性作为新的 VM 参数:
-
-Djava.util.logging.manager=org.jboss.logmanager.LogManager
-
-Dmaven.home=<path-to-your-maven-installation>
使用此 JRE 定义作为 Quarkus 项目的目标运行时,此替代方法将应用于任何“作为 JUnit 运行”配置。
Testing Dev Services
默认情况下,测试应该通过 Dev Services正常工作,但是从某些用例中你可能需要访问测试中的自动配置属性。
您可以通过 io.quarkus.test.common.DevServicesContext
来实现,可以直接将 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 实例、将数据添加到数据库等)。
对于 `@QuarkusIntegrationTest`测试,结果会将应用作为一个容器启动,`io.quarkus.test.common.DevServicesContext`还提供对该容器网络 ID 的访问权限,应用容器在此网络上启动(通过 `containerNetworkId`方法)。`QuarkusTestResourceLifecycleManager`可以使用此方法来启动应用需要与之通信的其他容器。 |
Testing Components
Quarkus 提供 QuarkusComponentTestExtension
,一个 JUnit 扩展,该扩展可简化组件的测试及其依赖项的模拟。这个 JUnit 扩展在 `quarkus-junit5-component`依赖项中可用。
我们来看一个组件 Foo
——一个包含两个注入点的 CDI Bean。
Foo
componentpackage 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`注入一个模拟对象。`Charlie`是 _unsatisifed_依赖项,系统会自动为其注册一个合成的 `@Singleton`Bean。注入的引用是一个“未配置”的 Mockito 模拟对象。 |
5 | 我们可以在一个测试方法中利用 Mockito API 来配置行为。 |
你可以在 testing components reference guide中找到更多示例和提示。