测试您的应用程序
学习如何测试您的Quarkus应用程序。本指南包括:
-
在JVM模式下测试
-
在本地模式下测试
-
将资源注入测试中
1. 先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
-
来自 《入门指南》 的完整的greeter应用程序
2. 架构
在本指南中,我们对作为入门指南的一部分而创建的初始测试进行扩展。我们涵盖了注入测试以及如何测试本地可执行文件。
Quarkus支持持续测试,但这是 持续测试指南 所讨论的。 |
3. 解决方案
我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。
克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或者下载 存档 。
The solution is located in the getting-started-testing
directory.
本指南假设您已经准备好了 getting-started
目录中的应用。
4. 对JVM模式中基于HTTP的测试的回顾
如果您已从入门的例子开始,那您应该已经有了一个完成的测试例子,包括正确的安装了工具。
在您的构建文件中应该有2个测试依赖:
quarkus-junit5
是测试所必需的,因为它提供 @QuarkusTest
注解来控制测试框架。而 rest-assured
不是必需的,但它是测试HTTP节点的一种便捷方式,我们还集成对自动设置正确URL的支持,因此不需要配置。
因为我们使用的是JUnit 5,所以必须设置 Surefire Maven插件 的版本,因为默认版本不支持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交互超时时间
当在您的测试中使用REST Assured时,连接和响应超时默认设置为30秒。可以通过 quarkus.http.test-timeout
属性覆盖该设置:
quarkus.http.test-timeout=10s
4.3. 注入URI
也可以直接将URL注入测试中,从而可以更容易的使用不同的客户端。可通过 @TestHTTPResource
注解完成。
让我们写一个简单的测试来演示如何加载一些静态资源。首先创建一个简单的HTML文件 src/main/resources/META-INF/resources/index.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 | 该注解允许您将URL直接注入为Quarkus实例,注解的值将是URL的路径 |
目前为止 @TestHTTPResource
允许您注入 URI
、 URL
以及 String
的URL表示。
5. 测试特定的节点
Both RESTassured and @TestHTTPResource
allow you to specify the endpoint class you are testing rather than hard coding
a path. This currently supports both Jakarta REST endpoints, Servlets and Reactive Routes. This makes it a lot easier to see exactly which endpoints
a given test is testing.
为了演示这些例子的目的,这里将假设我们有一个类似于下面的节点:
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
This currently does not support the @ApplicationPath() annotation to set the Jakarta REST context path. Use the
quarkus.resteasy.path config value instead if you want a custom context 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将被注入到测试类中 |
7. 在测试中使用拦截器
如上所述,Quarkus测试实际上是完整的CDI Bean,因此您可以自由的使用CDI拦截器。举个例子,如果您想让测试方法在事务上下文中运行,您可以将 @Transactional
注解加到该方法上,之后事务拦截器将会处理。
除此以外,您还可以创建您自己的测试 stereotypes。例如,我们可以创建一个 @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.QuarkusTestBeforeEachCallback
-
io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
-
io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
Optionally, you can enable these callbacks also for the @QuarkusIntegrationTest
tests if the property quarkus.test.enable-callbacks-for-integration-tests
is true
.
这种回调实现必须作为java "服务提供者(service provider) "注册,从而被 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
可以从测试类或方法中读取注解,从而控制回调应做什么。 |
While it is possible to use JUnit Jupiter callback interfaces like BeforeEachCallback , you might run into classloading issues because Quarkus has
to run tests in a custom classloader which JUnit is not aware of.
|
10. 测试不同的Profiles
到目前为止,在我们所有的例子中,我们只为所有的测试启动Quarkus一次。在第一个测试运行之前,Quarkus会启动,然后所有测试都会在这次启动中运行,最后Quarkus会关闭。这使得测试体验非常快,但是它有一点局限性,因为您无法测试不同的配置。
为了解决这个问题,Quarkus支持测试profile。如果一个测试有一个与之前运行的测试不同的profile,那么Quarkus将被停止,并在运行对应的测试之前使用新的profile启动。这显然会有些许变慢,因为它在测试时间上增加了一个停止/启动周期,但同时也提供了很大的灵活性。
为了减少Quarkus需要重启的次数, io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer
被注册为全局 ClassOrderer
,如 JUnit 5用户指南 中所述。这个orderer的行为可以通过 junit-platform.properties
(更多细节见源代码或javadoc)来配置。它也可以通过配置另一个由JUnit 5所提供的ClassOrderer,甚至是您自己的自定义Orderer来完全禁用。+ 请注意,从JUnit 5.8.2开始 只能有一个 junit-platform.properties
被使用,如果超过一个就会有警告记录 。如果您遇到这样的警告,您可以通过从classpath中移除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>
10.1. 编写Profile
为了实现一个测试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.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 | If a test profile implementation declares a CDI bean (via producer method/field or nested static class) then this bean is only taken into account if the test profile is used, i.e. it’s ignored for any other test profile. |
现在我们已经定义了我们自己的profile,我们需要在我们的测试类中引入它。我们通过在测试类中注解 @TestProfile(MockGreetingProfile.class)
来做到这一点。
所有的测试profile配置都存储在一个单一的类中,这使得我们很容易知道之前的测试是否以相同的配置运行。
10.2. 运行特定的测试
Quarkus提供了将测试限制在具有特定 @TestProfile
注释的测试中执行的能力。这是通过联合利用 QuarkusTestProfile
的 tags
方法和 quarkus.test.profile.tags
系统属性来实现的。
本质上,任何至少有一个标签与 quarkus.test.profile.tags
的值相匹配的 QuarkusTestProfile
将被认为是激活的,并且所有被激活的profiles中注解了 @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
实现。
11. Mock支持
Quarkus支持使用两种不同的方法来mock对象。您可以使用CDI alternatives来mock出所有测试类的Bean,也可以使用 QuarkusMock
来mock出每个测试类的Bean。
11.1. CDI @Alternative
机制。
要使用这个方法,只需用 src/test/java
目录中的一个类来覆盖您想mock的Bean,并在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 范围的覆盖。 |
注意,alternative要放置于 src/test/java
目录中而不是 src/main/java
,否则它将不仅仅只是在测试时生效。
另外需要注意的是,目前这种方法不能用于本地镜像测试,因为这需要将测试用的alternatives加入本地镜像中。
11.2. 使用QuarkusMock进行mock
io.quarkus.test.junit.QuarkusMock
类可以被用来临时mock任何正常scope的bean。如果您在 @BeforeAll
方法中使用这个方法,mock将对当前类的所有测试生效,而如果您在测试方法中使用这个方法,mock将只在当前测试方法范围内生效。
该方法可以用于任何正常scope的CDI Bean(例如: @ApplicationScoped
, @RequestScoped
等,基本上是除了 @Singleton
和 @Dependent
以外的scope )。
示例用法如下所示:
@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,您可以使用任何您喜欢的mocking库,甚至可以手动覆盖对象以提供您需要的行为。
Using @Inject will get you a CDI proxy to the mock instance you install, which is not suitable for passing to methods such as Mockito.verify
which want the mock instance itself.
So if you need to call methods such as verify you should hang on to the mock instance in your test, or use @io.quarkus.test.InjectMock .
|
11.2.1. 使用 @InjectMock
进行进一步简化
Building on the features provided by QuarkusMock
, Quarkus also allows users to effortlessly take advantage of Mockito for mocking the beans supported by QuarkusMock
.
This functionality is available with the
|
使用 @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 results in a Mockito mock being created, which is then available in test methods of the test class (other test classes are not affected by this) |
2 | mockableBean1 被mockito配置并可以为类的每个测试方法所用 |
3 | 由于 mockableBean2 mock还没有被配置,它将返回默认的Mockito响应。 |
4 | 在这个测试中, mockableBean2 进行了配置,所以它返回配置好的响应。 |
尽管上面的测试很好地展示了 @InjectMock
的能力,但它并不能很好地表示一个真实的测试案例。在一个真实的测试案例中,我们很可能会配置一个mock,然后测试一个使用了mocked 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的响应 |
By default, the @InjectMock
annotation can be used for any normal CDI scoped bean (e.g. @ApplicationScoped
, @RequestScoped
).
Mocking @Singleton
beans can be performed by adding the @MockitoConfig(convertScopes = true)
annotation.
This will convert the @Singleton
bean to an @ApplicationScoped
bean for the test.
这是一个高级选项,只有在您完全了解改变Bean scope的后果时才可以执行。
11.2.2. 通过 @InjectSpy
用Spies来代替Mocks
基于 InjectMock
所提供的功能,Quarkus还允许用户很容易利用 Mockito 来对 QuarkusMock
所支持的bean进行spy操作。这个功能可以通过 @io.quarkus.test.junit.mockito.InjectSpy
注解来实现,该注解通过 quarkus-junit5-mockito
依赖导入。
Sometimes when testing you only need to verify that a certain logical path was taken, or you only need to stub out a single method’s response while still executing the rest of the methods on the Spied clone. Please see Mockito documentation - Spying on real objects for more details on Spy partial mocks.
In either of those situations a Spy of the object is preferable.
Using @InjectSpy
, the previous example could be written as follows:
@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 | Here we are telling the Spy to return "hi" instead of "hello". When the GreetingResource requests the greeting from GreetingService we get the mocked response instead of the response of the regular GreetingService bean. Sometimes it’s impossible or impractical to use when(Object) for stubbing spies. Therefore when using spies please consider doReturn|Answer|Throw() family of methods for stubbing. |
3 | 这里我们正在验证从Spy那里得到了mock响应。 |
11.2.3. 使用 @InjectMock
与 @RestClient
The @RegisterRestClient
registers the implementation of the REST Client at runtime, and because the bean needs to be a regular scope, you have to annotate your interface with @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 的实例。 |
11.3. 使用Panache mock
如果您使用 quarkus-hibernate-orm-panache
或 quarkus-mongodb-panache
扩展,请查看 Hibernate ORM with Panache Mocking 和 MongoDB with Panache Mocking 文档,以了解mock数据访问的最简单方式。
12. 测试安全性
如果您正在使用Quarkus Security,请查看 测试安全 部分,了解如何轻松测试应用程序的安全功能。
13. 在Quarkus应用程序启动之前启动服务
一个很常见的需求是在Quarkus应用程序启动测试之前,启动一些您的Quarkus应用程序所依赖的服务。为了解决这个需求,Quarkus提供了 @io.quarkus.test.common.QuarkusTestResource
和 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
。
By simply annotating any test in the test suite with @QuarkusTestResource
, Quarkus will run the corresponding QuarkusTestResourceLifecycleManager
before any tests are run.
A test suite is also free to utilize multiple @QuarkusTestResource
annotations, in which case all the corresponding QuarkusTestResourceLifecycleManager
objects will be run before the tests.
测试资源是全局性的,即使它们被定义在一个测试类或自定义profile上,这意味着它们将全部被激活从而用于所有的测试,尽管我们删除了重复的测试。如果您只想针对一个测试类或测试profile启用某一个测试资源,您可以使用 @QuarkusTestResource(restrictToAnnotatedClass = true) 。
|
When using multiple test resources, they can be started concurrently. For that you need to set @QuarkusTestResource(parallel = true) .
|
Quarkus提供了一些开箱即用的 QuarkusTestResourceLifecycleManager
(见 io.quarkus.test.h2.H2DatabaseTestResource
,它启动了一个H2数据库;或 io.quarkus.test.kubernetes.client.KubernetesServerTestResource
,它启动了一个模拟的Kubernetes API服务器),但创建自定义的实现来满足特定的应用需求也是很常见的。常见的情况包括使用 Testcontainers 启动docker容器(例子 见这里 ),或使用 Wiremock 启动一个模拟的HTTP服务器(例子 见这里 )。
As QuarkusTestResourceLifecycleManager is not a CDI Bean, classes that implement it can’t have fields injected with @Inject . You can use String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class);
|
13.1. 改动测试类
When creating a custom QuarkusTestResourceLifecycleManager
that needs to inject something into the test class, the inject
methods can be used.
If for example you have a test like the following:
@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对测试类进行了所有必要的注入之后。 |
13.2. 基于注解的测试资源
我们也可以编写使用注解来启用和配置的测试资源。这可以通过在一个注解上使用 @QuarkusTestResource
来启用,该注解将被用来启用和配置测试资源。
例如,下面代码定义了 @WithKubernetesTestServer
注释,您可以在您的测试上使用它来激活 KubernetesServerTestResource
,但只针对被注释的测试类。您也可以把它们加入到您的 QuarkusTestProfile
测试profile中。
@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();
}
// ...
}
If you want to make the annotation repeatable, the containing annotation type must be annotated with @QuarkusTestResourceRepeatable
.
For example, this would define a repeatable @WithRepeatableTestResource
annotation.
@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. 挂起侦测
@QuarkusTest
支持挂起侦测,以帮助诊断任何意外的挂起。如果在指定的时间内没有进展(即没有调用JUnit回调),那么Quarkus将打印一个堆栈跟踪到控制台以帮助诊断挂起。这个超时的默认值是10分钟。
不会有进一步的动作执行,测试将继续正常进行(通常直到CI超时),但是打印出来的堆栈信息应该有助于诊断为什么构建失败了。您可以用 quarkus.test.hang-detection-timeout
系统属性来控制这个超时值(您也可以在application.properties中设置这个值,但是在Quarkus启动之前它不会被读取,所以Quarkus启动的默认超时时间将是10分钟)。
15. 本地可执行程序测试
您也可以使用 @QuarkusIntegrationTest
来测试本地可执行文件。除了注入测试(本地可执行文件在一个单独的非JVM进程中运行,所以它实际上是不可能做到的)以外,该特性支持本指南中提到的所有功能。
这在《 本地可执行文件指南》 中有所涉及。
16. 使用@QuarkusIntegrationTest
@QuarkusIntegrationTest
should be used to launch and test the artifact produced by the Quarkus build, and supports testing a jar (of whichever type), a native image or container image.
Put simply, this means that if the result of a Quarkus build (mvn package
or gradle build
) is a jar, that jar will be launched as java -jar …
and tests run against it.
If instead a native image was built, then the application is launched as ./application …
and again the tests run against the running application.
Finally, if a container image was created during the build (by including the quarkus-container-image-jib
, quarkus-container-image-docker
, or container-image-podman
extensions and having the
quarkus.container-image.build=true
property configured), then a container is created and run (this requires the docker
or podman
executable being present).
This is a black box test that supports the same set features and has the same limitations.
由于用 |
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去运行继承测试。
然后,打开 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 | 这里使用另一个测试runner来通过本地文件在测试之前启动应用。该执行文件通过 Failsafe Maven Plugin 来获取。 |
2 | 为了方便起见,我们扩展了之前的测试,但您也可以实现您自己的测试。 |
更多的信息可以在 Testing the native executable Guide 里找到。
When the application is tested using |
While adding test-specific configuration properties using |
16.1. 启动容器
当 @QuarkusIntegrationTest
启动一个容器时(因为应用程序在构建时将 quarkus.container-image.build
设置为 true
),该容器会在一个可预测的容器网络上启动。这有利于编写需要启动服务以支持应用程序的集成测试。这意味着 @QuarkusIntegrationTest
能够与通过 Dev Services 启动的容器一起开箱即用,但这也意味着它能够使用 QuarkusTestLifecycleManager 资源来启动额外的容器。这可以通过让您的 QuarkusTestLifecycleManager
实现 io.quarkus.test.common.DevServicesContext.ContextAware
来获得。一个简单的例子:
运行要测试资源的容器,例如通过Testcontainers启用的PostgreSQL,会从容器的网络中分配到一个IP地址。使用容器网络中的 "公共 "IP和 "未映射 "的端口号来连接到服务。Testcontainers库通常在不遵从容器网络规则的情况下返回连接字符串,所以需要额外的代码来为Quarkus提供 "正确的 "连接字符串,以使用容器网络中的IP和 未映射 的端口号。
下面的例子展示了在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
将在 @QuarkusIntegrationTest
上通过使用 @QuarkusTestResource
激活,就如本文档对应部分所述。
16.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
To test against a running instance that only accepts SSL/TLS connection (example: https://1.2.3.4:4321
) set the system property quarkus.http.test-ssl-enabled
to true
.
17. 混合使用 @QuarkusTest
与其他类型的测试
在一次执行中(例如在一次Maven Surefire Plugin执行中),将注释为 @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>
Currently For Maven, this means that the former should be run by the surefire plugin while the latter should be run by the failsafe plugin. For Gradle, this means the two types of tests should belong to different source sets. Source set configuration example
|
18. 从IDE中运行 @QuarkusTest
大多数IDE都提供了将选定的类直接作为JUnit测试运行的可能性。为了做到这点,您需要在您选择的IDE的设置中设置一些属性:
18.1. Eclipse中独立的JRE定义
将您当前的 "已安装的JRE "定义复制为一个新的定义,在这里您会将其作为新的虚拟机参数配置来添加属性:
-
-Djava.util.logging.manager=org.jboss.logmanager.LogManager
-
-Dmaven.home=<path-to-your-maven-installation>
使用这个JRE定义作为您的Quarkus项目的目标运行时,该运行时将被用于所有 "作为JUnit运行(Run as JUnit) "的配置。
19. 测试开发服务
默认情况下,测试应该只与 开发服务 一起工作,然而在一些用例中,您可能需要访问测试中自动配置的属性。
您可以使用 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实例,向数据库添加数据等)。
对于将应用程序作为容器启动的 |
20. Testing Components
Quarkus provides the QuarkusComponentTestExtension
, a JUnit extension to ease the testing of components and mocking of their dependencies.
This JUnit extension is available in the quarkus-junit5-component
dependency.
Let’s have a component Foo
- a CDI bean with two injection points.
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 is an @ApplicationScoped CDI bean. |
2 | Foo depends on Charlie which declares a method ping() . |
3 | Foo depends on the config property bar . @Inject is not needed for this injection point because it also declares a CDI qualifier - this is a Quarkus-specific feature. |
Then a component test could look like:
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 | The QuarkusComponentTest annotation registers the JUnit extension. |
2 | Sets a configuration property for the test. |
3 | The test injects the component under the test. The types of all fields annotated with @Inject are considered the component types under test. You can also specify additional component classes via @QuarkusComponentTest#value() . Furthermore, the static nested classes declared on the test class are components too. |
4 | The test also injects a mock for Charlie . Charlie is an unsatisfied dependency for which a synthetic @Singleton bean is registered automatically. The injected reference is an "unconfigured" Mockito mock. |
5 | We can leverage the Mockito API in a test method to configure the behavior. |
You can find more examples and hints in the testing components reference guide.