The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

测试您的应用程序

学习如何测试您的Quarkus应用程序。本指南包括:

  • 在JVM模式下测试

  • 在本地模式下测试

  • 将资源注入测试中

1. 先决条件

要完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • 安装JDK 11以上版本并正确配置了 JAVA_HOME

  • Apache Maven 3.8.1+

  • 如果你愿意的话,还可以选择使用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个测试依赖:

Maven
<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>
Gradle
dependencies {
    testImplementation("io.quarkus:quarkus-junit5")
    testImplementation("io.rest-assured:rest-assured")
}

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.http.test-portquarkus.http.test-ssl-port 来配置用于HTTP和HTTPS测试的端口:

quarkus.http.test-port=8083
quarkus.http.test-ssl-port=8446

设置为 0 则会使用随机端口(由操作系统分配)。

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 允许您注入 URIURL 以及 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

这种回调实现必须作为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 注释的测试中执行的能力。这是通过联合利用 QuarkusTestProfiletags 方法和 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 :在这种情况下, SingleTagTestMultipleTagsTest 将被运行,因为它们各自的 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"。当 GreetingResourceGreetingService 请求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-panachequarkus-mongodb-panache 扩展,请查看 Hibernate ORM with Panache MockingMongoDB with Panache Mocking 文档,以了解mock数据访问的最简单方式。

12. 测试安全性

如果您正在使用Quarkus Security,请查看 测试安全 部分,了解如何轻松测试应用程序的安全功能。

13. 在Quarkus应用程序启动之前启动服务

一个很常见的需求是在Quarkus应用程序启动测试之前,启动一些您的Quarkus应用程序所依赖的服务。为了解决这个需求,Quarkus提供了 @io.quarkus.test.common.QuarkusTestResourceio.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-jibquarkus-container-image-docker 扩展并使用了 quarkus.container-image.build=true 属性),那么将创建并运行一个容器(这需要 docker 可执行文件的存在)。

如同 @NativeImageTest ,这是一个黑盒测试,支持相同的功能集且具有相同的限制。

由于用 @QuarkusIntegrationTest 注释的测试是对构建结果的测试,它应该作为集成测试套件的一部分来运行—​即如果使用Maven,则设置 -DskipITs=false,而如果使用Gradle,则通过 quarkusIntTest 任务。如果与 @QuarkusTest 在同一阶段运行,这些测试将 无法 工作,因为Quarkus还没有产生出最终的artifact。

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的设置中设置一些属性:

  • java.util.logging.manager (见 日志指南 )

  • maven.home (仅当 ${maven.home}/conf/settings.xml 中有自定义设置时,见Maven指南 )

  • maven.settings (以备在测试中使用自定义的 settings.xml 文件)

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) "的配置。

18.2. VSCode "run with "配置

在您的项目根目录或工作区的 settings.json 文件中,针对测试配置添加以下配置项目:

"java.test.config": [
    {
        "name": "quarkusConfiguration",
        "vmargs": [ "-Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dmaven.home=<path-to-your-maven-installation> ..." ],
        ...
    },
  ...
]

18.3. IntelliJ IDEA JUnit模板

在IntelliJ中不需要任何改动,因为IDE会从 pom.xml 中的surefire插件配置中选择 systemPropertyVariables

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实例,向数据库添加数据等)。

对于将应用程序作为容器启动的 @QuarkusIntegrationTest 测试,io.quarkus.test.common.DevServicesContext 也提供了对应用容器所启动的容器网络id的访问(通过 containerNetworkId 方法)。这可以被 QuarkusTestResourceLifecycleManager 使用来启动一些应用程序需要通信的其他容器。