测试您的应用程序
学习如何测试您的Quarkus应用程序。本指南包括:
-
在JVM模式下测试
-
在本地模式下测试
-
将资源注入测试中
1. 先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
安装JDK 11以上版本并正确配置了
JAVA_HOME
-
Apache Maven 3.8.6
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
-
来自 《入门指南》 的完整的greeter应用程序
2. 架构
在本指南中,我们对作为入门指南的一部分而创建的初始测试进行扩展。我们涵盖了注入测试以及如何测试本地可执行文件。
Quarkus支持持续测试,但这是 持续测试指南 所讨论的。 |
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插件 的版本,因为默认版本不支持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. 测试特定的节点
RESTassured和 @TestHTTPResource
都允许您指定要测试的节点类,而不是对路径硬编码。这目前支持JAX-RS节点、Servlets和Reactive Routes。这使您更容易查看给定的测试用例到底在测试哪些节点。
为了演示这些例子的目的,这里将假设我们有一个类似于下面的节点:
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
目前不支持使用 @ApplicationPath() 注解来设置 JAX-RS 上下文路径。如果您想要一个自定义的上下文路径,请使用 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 javax.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
可以从测试类或方法中读取注解,从而控制回调应做什么。 |
虽然可以使用JUnit Jupiter回调接口,如 BeforeEachCallback ,但您可能会遇到类加载问题,因为Quarkus必须在一个JUnit无法感知的自定义类加载器中运行测试。
|
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 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 JAX-RS 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;
}
}
1 | 所有这些方法都有默认的实现,所以只要覆盖您需要覆盖的方法就可以。 |
现在我们已经定义了我们自己的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 Collections.singleton("test1");
}
}
public static class MultipleTags implements QuarkusTestProfile {
@Override
public Set<String> tags() {
return new HashSet<>(Arrays.asList("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库,甚至可以手动覆盖对象以提供您需要的行为。
使用 @Inject ,您会得到一个对您安装的mock实例的CDI代理,它并不适合传递给诸如 Mockito.verify 这样的方法,这些方法会需要mock实例本身。因此,如果您需要调用诸如 verify 这样的方法,您需要在您的测试中挂载mock实例,或者使用 @InjectMock ,如下所示。
|
11.2.1. 使用 @InjectMock
进行进一步简化
基于 QuarkusMock
所提供的功能,Quarkus还允许用户很容易地利用 Mockito 来mock QuarkusMock
所支持的bean。该功能可以通过 @io.quarkus.test.junit.mockito.InjectMock
注解来实现,该注解可以通过添加 quarkus-junit5-mockito
依赖而导入。
使用 @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 可以生成一个mock并使其在测试类的所有测试方法中可用(其他测试 类*不受* 此影响) |
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的响应 |
默认情况下, @InjectMock
注解可用于任何正常的CDI scoped的Bean(例如 @ApplicationScoped
, @RequestScoped
)。如果要mock @Singleton
Bean则可以通过设置 convertScopes
属性为true来达到目的(如 @InjectMock(convertScopes = true)
)。这会把 @Singleton
bean转换为 @ApplicationScoped
bean来测试。
这是一个高级选项,只有在您完全了解改变Bean scope的后果时才可以执行。
11.2.2. 通过 @InjectSpy
用Spies来代替Mocks
基于 InjectMock
所提供的功能,Quarkus还允许用户很容易利用 Mockito 来对 QuarkusMock
所支持的bean进行spy操作。这个功能可以通过 @io.quarkus.test.junit.mockito.InjectSpy
注解来实现,该注解通过 quarkus-junit5-mockito
依赖导入。
有时在测试时您只需要验证某个逻辑路径,或者您只想对某一个方法的响应进行stub,同时仍保持执行Spied clone上的其他方法。关于Spy部分mock的更多细节,请参见 Mockito文档 。在这两种情况下,推荐使用针对对象的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() {
when(greetingService.greet()).thenReturn("hi"); (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 请求greeting时,我们得到的是mock的响应,而不是正常的 GreetingService Bean的响应 |
3 | 这里我们正在验证从Spy那里得到了mock响应。 |
11.2.3. 使用 @InjectMock
与 @RestClient
@RegisterRestClient
在运行时注册了rest-client的实现,并且由于bean需要保证使用了一个正常的scope,您必须使用 @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
。
通过简单地在测试套件中用 @QuarkusTestResource
注释测试,Quarkus将在测试运行之前运行相应的 QuarkusTestResourceLifecycleManager
。测试套件也可以自由地利用多个 @QuarkusTestResource
注释,从而使所有对应的 QuarkusTestResourceLifecycleManager
对象在测试前运行。当使用多个测试资源时,它们可以并行启动。为此,您需要设置 @QuarkusTestResource(parallel = true)
。
测试资源是全局性的,即使它们被定义在一个测试类或自定义profile上,这意味着它们将全部被激活从而用于所有的测试,尽管我们删除了重复的测试。如果您只想针对一个测试类或测试profile启用某一个测试资源,您可以使用 @QuarkusTestResource(restrictToAnnotatedClass = true) 。
|
Quarkus提供了一些开箱即用的 QuarkusTestResourceLifecycleManager
(见 io.quarkus.test.h2.H2DatabaseTestResource
,它启动了一个H2数据库;或 io.quarkus.test.kubernetes.client.KubernetesServerTestResource
,它启动了一个模拟的Kubernetes API服务器),但创建自定义的实现来满足特定的应用需求也是很常见的。常见的情况包括使用 Testcontainers 启动docker容器(例子 见这里 ),或使用 Wiremock 启动一个模拟的HTTP服务器(例子 见这里 )。
13.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对测试类进行了所有必要的注入之后。 |
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();
}
// ...
}
14. 挂起侦测
@QuarkusTest
支持挂起侦测,以帮助诊断任何意外的挂起。如果在指定的时间内没有进展(即没有调用JUnit回调),那么Quarkus将打印一个堆栈跟踪到控制台以帮助诊断挂起。这个超时的默认值是10分钟。
不会有进一步的动作执行,测试将继续正常进行(通常直到CI超时),但是打印出来的堆栈信息应该有助于诊断为什么构建失败了。您可以用 quarkus.test.hang-detection-timeout
系统属性来控制这个超时值(您也可以在application.properties中设置这个值,但是在Quarkus启动之前它不会被读取,所以Quarkus启动的默认超时时间将是10分钟)。
15. 本地可执行程序测试
您也可以使用 @QuarkusIntegrationTest
来测试本地可执行文件。除了注入测试(本地可执行文件在一个单独的非JVM进程中运行,所以它实际上是不可能做到的)以外,该特性支持本指南中提到的所有功能。
这在《 本地可执行文件指南》 中有所涉及。
16. 使用@QuarkusIntegrationTest
@QuarkusIntegrationTest
应该被用来启动和测试由Quarkus构建产生的物件,而且支持测试一个jar(无论哪种类型),一个本地镜像或容器镜像。简单地说,这意味着如果Quarkus构建( mvn package
或者 gradle build
)的结果是一个jar,这个jar将以 java -jar …
方式启动 ,并针对它运行测试。如果构建的是一个本地镜像,那么应用程序将以 ./application …
方式启动,并再次针对运行中的应用程序进行测试。最后,如果在构建过程中创建了一个容器镜像(通过引入 quarkus-container-image-jib
或 quarkus-container-image-docker
扩展并使用了 quarkus.container-image.build=true
属性),那么将创建并运行一个容器(这需要 docker
可执行文件的存在)。
如同 @NativeImageTest
,这是一个黑盒测试,支持相同的功能集且具有相同的限制。
由于用 |
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)
// 运行相同测试
}
1 | 这里使用另一个测试runner来通过本地文件在测试之前启动应用。该执行文件通过 Failsafe Maven Plugin 来获取。 |
2 | 为了方便起见,我们扩展了之前的测试,但您也可以实现您自己的测试。 |
更多的信息可以在 Testing the native executable Guide 里找到。
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. 对正在运行的应用程序执行测试
这个功能是实验性的,在未来的Quarkus版本中可能会有变化。 |
@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
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>
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实例,向数据库添加数据等)。
对于将应用程序作为容器启动的 |