Testing Your Application

了解如何测试 Quarkus 应用程序。本指南涵盖:

Learn how to test your Quarkus Application. This guide covers:

  • Testing in JVM mode

  • Testing in native mode

  • Injection of resources into tests

Prerequisites

include::{includes}/prerequisites.adoc[]* Getting Started Guide的完成问候应用程序

Unresolved directive in getting-started-testing.adoc - include::{includes}/prerequisites.adoc[] * The completed greeter application from the Getting Started Guide

Architecture

在本指南中,我们扩展了入门指南中创建的初始测试。我们涵盖了向测试中注入以及如何测试原生可执行文件。

In this guide, we expand on the initial test that was created as part of the Getting Started Guide. We cover injection into tests and also how to test native executables.

Quarkus 支持持续测试,但这由 Continuous Testing Guide涵盖。

Quarkus supports Continuous testing, but this is covered by the Continuous Testing Guide.

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

该解决方案位于 getting-started-testing directory

The solution is located in the getting-started-testing directory.

本指南假设您已经有了 `getting-started`目录中完成的应用程序。

This guide assumes you already have the completed application from the getting-started directory.

Recap of HTTP based Testing in JVM mode

如果您已经从入门示例开始,您应该已经有一个完成的测试,包括正确的工具设置。

If you have started from the Getting Started example you should already have a completed test, including the correct tooling setup.

在您的构建文件中,您应该看到 2 个测试依赖项:

In your build file you should see 2 test dependencies:

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,因此无需配置。

quarkus-junit5 is required for testing, as it provides the @QuarkusTest annotation that controls the testing framework. rest-assured is not required but is a convenient way to test HTTP endpoints, we also provide integration that automatically sets the correct URL so no configuration is required.

由于我们使用的是 JUnit 5,因此必须设置 Surefire Maven Plugin的版本,因为默认版本不支持 Junit 5:

Because we are using JUnit 5, the version of the Surefire Maven Plugin must be set, as the default version does not support 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`中的自定义配置(如果有)。

We also set the java.util.logging.manager system property to make sure tests will use the correct logmanager and maven.home to ensure that custom configuration from ${maven.home}/conf/settings.xml is applied (if any).

该项目还应该包含一个简单的测试:

The project should also contain a simple test:

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 端点。在运行测试之前,应用程序将在测试运行之前启动。

This test uses HTTP to directly test our REST endpoint. When the test is run the application will be started before the test is run.

Controlling the test port

虽然 Quarkus 默认侦听端口 8080,但它在运行测试时会默认使用 8081。这样您就可以同时运行测试,同时应用程序也在并行运行。

While Quarkus will listen on port 8080 by default, when running tests it defaults to 8081. This allows you to run tests while having the application running in parallel.

Changing the test port

您可以通过在 application.properties 中配置 HTTP 的 quarkus.http.test-port 和 HTTPS 的 quarkus.http.test-ssl-port 来配置测试使用的端口:

You can configure the ports used by tests by configuring quarkus.http.test-port for HTTP and quarkus.http.test-ssl-port for HTTPS in your application.properties:

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

0 将导致使用随机端口(由操作系统分配)。

0 will result in the use of a random port (assigned by the operating system).

Quarkus 还提供了 RestAssured 集成,可以在运行测试之前更新 RestAssured 使用的默认端口,因此无需额外的配置。

Quarkus also provides RestAssured integration that updates the default port used by RestAssured before the tests are run, so no additional configuration should be required.

Controlling HTTP interaction timeout

在测试中使用 REST Assured 时,连接和响应超时时间设置为 30 秒。您可以使用 quarkus.http.test-timeout 属性覆盖此设置:

When using REST Assured in your test, the connection and response timeouts are set to 30 seconds. You can override this setting with the quarkus.http.test-timeout property:

quarkus.http.test-timeout=10s

Injecting a URI

还可以将 URL 直接注入到测试中,这可以轻松使用不同的客户端。这是通过 @TestHTTPResource 注解完成的。

It is also possible to directly inject the URL into the test which can make is easy to use a different client. This is done via the @TestHTTPResource annotation.

我们来编写一个简单的测试来展示如何加载一些静态资源。首先在 src/main/resources/META-INF/resources/index.html 中创建一个简单的 HTML 文件:

Let’s write a simple test that shows this off to load some static resources. First create a simple HTML file in src/main/resources/META-INF/resources/index.html :

<html>
    <head>
        <title>Testing Guide</title>
    </head>
    <body>
        Information about testing
    </body>
</html>

我们将创建一个简单的测试来确保正确提供服务:

We will create a simple test to ensure that this is being served correctly:

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 This annotation allows you to directly inject the URL of the Quarkus instance, the value of the annotation will be the path component of the URL

现在,@TestHTTPResource 允许您注入 URIURLString 的 URL 表示。

For now @TestHTTPResource allows you to inject URI, URL and String representations of the URL.

Testing a specific endpoint

RESTassured 和 @TestHTTPResource 允许您指定要测试的端点类,而不是对路径进行硬编码。它目前支持 Jakarta REST 端点、Servlet 和 Reactive Routes。这使得查看给定测试正在测试的确切端点变得更加容易。

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.

为了这些示例的目的,我将假设我们有一个类似于以下内容的端点:

For the purposes of these examples I am going to assume we have an endpoint that looks like the following:

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

当前不支持使用 @ApplicationPath() 注解来设置 Jakarta REST 上下文路径。如果您希望使用自定义上下文路径,请改用 quarkus.resteasy.path 配置值。

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.

TestHTTPResource

您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint 注解来指定端点路径,并且路径将从提供的端点中提取出来。如果您还为 TestHTTPResource 端点指定了一个值,它将附加到端点路径的末尾。

You can the use the io.quarkus.test.common.http.TestHTTPEndpoint annotation to specify the endpoint path, and the path will be extracted from the provided endpoint. If you also specify a value for the TestHTTPResource endpoint it will be appended to the end of the endpoint path.

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 Because GreetingResource is annotated with @Path("/hello") the injected URL will end with /hello.

RESTassured

要控制 RESTassured 基本路径(即作为每个请求的根的默认路径),可以使用 io.quarkus.test.common.http.TestHTTPEndpoint 注解。这可以应用于类或方法级别。要测试问候资源,我们可以执行以下操作:

To control the RESTassured base path (i.e. the default path that serves as the root for every request) you can use the io.quarkus.test.common.http.TestHTTPEndpoint annotation. This can be applied at the class or method level. To test out greeting resource we would do:

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 This tells RESTAssured to prefix all requests with /hello.
2 Note we don’t need to specify a path here, as /hello is the default for this test

Injection into tests

到目前为止,我们仅介绍了通过 HTTP 端点测试应用程序的集成式测试,但如果我们希望进行单元测试并直接测试我们的 bean 该怎么办?

So far we have only covered integration style tests that test the app via HTTP endpoints, but what if we want to do unit testing and test our beans directly?

Quarkus 支持这一点,允许您通过 @Inject 注释将 CDI bean 注入测试(实际上,Quarkus 中的测试就是完整的 CDI bean,因此您可以使用所有 CDI 功能)。我们创建一个无需使用 HTTP 即可直接测试 GreetingService 的简单测试:

Quarkus supports this by allowing you to inject CDI beans into your tests via the @Inject annotation (in fact, tests in Quarkus are full CDI beans, so you can use all CDI functionality). Let’s create a simple test that tests the greeting service directly without using 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 The GreetingService bean will be injected into the test

Applying Interceptors to Tests

如上文所述,Quarkus 测试实际上就是完整的 CDI bean,因此您可以按照正常方式应用 CDI 拦截器。例如,如果您希望某个测试方法在事务上下文中运行,只需将 @Transactional 注释应用于该方法即可,事务拦截器将处理它。

As mentioned above Quarkus tests are actually full CDI beans, and as such you can apply CDI interceptors as you would normally. As an example, if you want a test method to run within the context of a transaction you can simply apply the @Transactional annotation to the method and the transaction interceptor will handle it.

除此之外,您还可以创建自己的测试模式。例如,我们可以创建 @TransactionalQuarkusTest 如下:

In addition to this you can also create your own test stereotypes. For example, we could create a @TransactionalQuarkusTest as follows:

@QuarkusTest
@Stereotype
@Transactional
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TransactionalQuarkusTest {
}

如果我们将此注释应用于测试类,则它的作用就如同应用了 @QuarkusTest@Transactional 注释,例如:

If we then apply this annotation to a test class it will act as if we had applied both the @QuarkusTest and @Transactional annotations, e.g.:

@TransactionalQuarkusTest
public class TestStereotypeTestCase {

    @Inject
    UserTransaction userTransaction;

    @Test
    public void testUserTransaction() throws Exception {
        Assertions.assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus());
    }

}

Tests and Transactions

您可以在测试中使用标准 Quarkus @Transactional 注释,但这意味着您的测试对数据库所做的更改将是持久的。如果您希望在测试结束时将进行的任何更改回滚,则可以使用 io.quarkus.test.TestTransaction 注释。这将在事务中运行测试方法,但在测试方法完成后将其回滚,以还原任何数据库更改。

You can use the standard Quarkus @Transactional annotation on tests, but this means that the changes your test makes to the database will be persistent. If you want any changes made to be rolled back at the end of the test you can use the io.quarkus.test.TestTransaction annotation. This will run the test method in a transaction, but roll it back once the test method is complete to revert any database changes.

Enrichment via QuarkusTest*Callback

或者除了拦截器之外,您还可以通过实现以下回调接口来丰富 all 您的 @QuarkusTest 类:

Alternatively or additionally to an interceptor, you can enrich all your @QuarkusTest classes by implementing the following callback interfaces:

  • io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback

  • io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback

  • io.quarkus.test.junit.callback.QuarkusTestBeforeTestExecutionCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterTestExecutionCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback

根据需要,如果属性 quarkus.test.enable-callbacks-for-integration-teststrue ,还可以为 @QuarkusIntegrationTest 测试启用这些回调。

Optionally, you can enable these callbacks also for the @QuarkusIntegrationTest tests if the property quarkus.test.enable-callbacks-for-integration-tests is true.

此类回调实现必须按照 java.util.ServiceLoader 定义注册为“服务提供程序”。

Such a callback implementation has to be registered as a "service provider" as defined by java.util.ServiceLoader.

例如,以下示例回调:

E.g. the following sample callback:

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 注册,如下所示:

has to be registered via src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback as follows:

org.acme.getting.started.testing.MyQuarkusTestBeforeEachCallback

可以从测试类或方法中读取注释来控制回调将执行的操作。

It is possible to read annotations from the test class or method to control what the callback shall be doing.

虽然可以使用 JUnit Jupiter 回调接口(例如 BeforeEachCallback ),但您可能会遇到类加载问题,因为 Quarkus 必须在 JUnit 不了解的自定义类加载器中运行测试。

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.

Testing Different Profiles

到目前为止,在所有示例中,我们只对所有测试启动一次 Quarkus。在运行第一个测试之前,Quarkus 将启动,然后所有测试将运行,最后 Quarkus 将关闭。这会形成非常快速的测试体验,但它会受到一点限制,因为您无法测试不同的配置。

So far in all our examples we only start Quarkus once for all tests. Before the first test is run Quarkus will boot, then all tests will run, then Quarkus will shut down at the end. This makes for a very fast testing experience however it is a bit limited as you can’t test different configurations.

为了解决这个问题,Quarkus 支持测试配置文件。如果某个测试具有与先前运行的测试不同的配置文件,则在运行测试之前,Quarkus 将关闭并使用新配置文件启动。这显然有点慢,因为它在测试时间中增加了一个关闭/启动周期,但它提供了很大的灵活性。

To get around this Quarkus supports the idea of a test profile. If a test has a different profile to the previously run test then Quarkus will be shut down and started with the new profile before running the tests. This is obviously a bit slower, as it adds a shutdown/startup cycle to the test time, but gives a great deal of flexibility.

为了减少 Quarkus 需要重新启动的次数,io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer 被注册为一个全局 ClassOrderer ,如 JUnit 5 User Guide 中所述。此 ClassOrderer 的行为可通过 junit-platform.properties 进行配置(请参阅源代码或 javadoc 以了解更多详细信息)。还可以通过设置另一个由 JUnit 5 提供的 ClassOrderer 甚至设置您自己的自定义 ClassOrderer 来完全禁用它。请注意,自 JUnit 5.8.2 起, only a single junit-platform.properties is picked up and a warning is logged if more than one is found 。如果您遇到此类警告,可以通过排除从类路径中移除 Quarkus 提供的 junit-platform.properties 来消除它们:

To reduce the amount of times Quarkus needs to restart, io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer is registered as a global ClassOrderer as described in the JUnit 5 User Guide. The behavior of this ClassOrderer is configurable via junit-platform.properties (see the source code or javadoc for more details). It can also be disabled entirely by setting another ClassOrderer that is provided by JUnit 5 or even your own custom one. Please note that as of JUnit 5.8.2 only a single junit-platform.properties is picked up and a warning is logged if more than one is found. If you encounter such warnings, you can get rid of them by removing the Quarkus-supplied junit-platform.properties from the classpath via an exclusion:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5-properties</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Writing a Profile

要实现测试配置文件,我们需要实现 io.quarkus.test.junit.QuarkusTestProfile

To implement a test profile we need to implement io.quarkus.test.junit.QuarkusTestProfile:

package org.acme.getting.started.testing;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.enterprise.inject.Produces;

import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;

public class MockGreetingProfile implements QuarkusTestProfile { 1

    /**
     * Returns additional config to be applied to the test. This
     * will override any existing config (including in application.properties),
     * however existing config will be merged with this (i.e. application.properties
     * config will still take effect, unless a specific config key has been overridden).
     *
     * Here we are changing the Jakarta REST root path.
     */
    @Override
    public Map<String, String> getConfigOverrides() {
        return Collections.singletonMap("quarkus.resteasy.path","/api");
    }

    /**
     * Returns enabled alternatives.
     *
     * This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key,
     * however it may be more convenient.
     */
    @Override
    public Set<Class<?>> getEnabledAlternatives() {
        return Collections.singleton(MockGreetingService.class);
    }

    /**
     * Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system
     * property before the test is run.
     *
     * Here we are setting the profile to test-mocked
     */
    @Override
    public String getConfigProfile() {
        return "test-mocked";
    }

    /**
     * Additional {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this
     * specific test profile.
     *
     * If this method is not overridden, then only the {@link QuarkusTestResourceLifecycleManager} classes enabled via the {@link io.quarkus.test.common.WithTestResource} class
     * annotation will be used for the tests using this profile (which is the same behavior as tests that don't use a profile at all).
     */
    @Override
    public List<TestResourceEntry> testResources() {
        return Collections.singletonList(new TestResourceEntry(CustomWireMockServerManager.class));
    }


    /**
     * If this returns true then only the test resources returned from {@link #testResources()} will be started,
     * global annotated test resources will be ignored.
     */
    @Override
    public boolean disableGlobalTestResources() {
        return false;
    }

    /**
     * The tags this profile is associated with.
     * When the {@code quarkus.test.profile.tags} System property is set (its value is a comma separated list of strings)
     * then Quarkus will only execute tests that are annotated with a {@code @TestProfile} that has at least one of the
     * supplied (via the aforementioned system property) tags.
     */
    @Override
    public Set<String> tags() {
        return Collections.emptySet();
    }

    /**
     * The command line parameters that are passed to the main method on startup.
     */
    @Override
    public String[] commandLineParameters() {
        return new String[0];
    }

    /**
     * If the main method should be run.
     */
    @Override
    public boolean runMainMethod() {
        return false;
    }

    /**
     * If this method returns true then all {@code StartupEvent} and {@code ShutdownEvent} observers declared on application
     * beans should be disabled.
     */
    @Override
    public boolean disableApplicationLifecycleObservers() {
        return false;
    }

    @Produces 2
    public ExternalService mockExternalService() {
       return new ExternalService("mock");
    }
}
1 All these methods have default implementations so just override the ones you need to override.
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.

现在我们已经定义了我们的配置文件,我们需要将其包含在我们的测试类中,我们可以通过使用 `@TestProfile(MockGreetingProfile.class)`注释测试类来实现此目的。

Now we have defined our profile we need to include it on our test class. We do this by annotating the test class with @TestProfile(MockGreetingProfile.class).

所有测试配置文件配置存储在一个类中,这使得我们可以轻松判断上一项测试是否使用相同的配置运行。

All the test profile configuration is stored in a single class, which makes it easy to tell if the previous test ran with the same configuration.

Running specific tests

Quarkus 提供了限制测试执行范围的功能,以只执行带有特定 @TestProfile`注释的测试,这通过利用 `QuarkusTestProfile 中的 tags 方法与 quarkus.test.profile.tags 系统属性共同作用来实现。

Quarkus provides the ability to limit test execution to tests with specific @TestProfile annotations. This works by leveraging the tags method of QuarkusTestProfile in conjunction with the quarkus.test.profile.tags system property.

本质上,任何在 quarkus.test.profile.tags 的值匹配了至少一个匹配标签的 QuarkusTestProfile 都将被视为处于活动状态且所有使用处于活动配置文件的 @TestProfile 注释的测试都将运行,而其余的测试则会被跳过,以下示例对此进行了最佳展示。

Essentially, any QuarkusTestProfile with at least one matching tag matching the value of quarkus.test.profile.tags will be considered active and all the tests annotated with @TestProfile of active profiles, will be run while the rest will be skipped. This is best shown in the following example.

让我们首先定义一些 QuarkusTestProfile 实现:

First let’s define a few QuarkusTestProfile implementations like so:

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");
        }
    }
}

现在让我们假设我们有以下测试:

Now let’s assume that we have the following tests:

@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
    }
}

让我们考虑以下场景:

Let’s consider the following scenarios:

  • quarkus.test.profile.tags is not set: All tests will be executed.

  • quarkus.test.profile.tags=foo: In this case none of tests will be executed because none of the tags defined on the QuarkusTestProfile implementations match the value of quarkus.test.profile.tags. Note that NoQuarkusProfileTest is not executed either because it is not annotated with @TestProfile.

  • quarkus.test.profile.tags=test1: In this case SingleTagTest and MultipleTagsTest will be run because the tags on their respective QuarkusTestProfile implementations match the value of quarkus.test.profile.tags.

  • quarkus.test.profile.tags=test1,test3: This case results in the same tests being executed as the previous case.

  • quarkus.test.profile.tags=test2,test3: In this case only MultipleTagsTest will be run because MultipleTagsTest is the only QuarkusTestProfile implementation whose tags method matches the value of quarkus.test.profile.tags.

Mock Support

Quarkus 支持使用两种不同的方法来使用模拟对象,你可以使用 CDI 替代来模拟所有测试类的 bean,也可以使用 QuarkusMock 来逐个测试模拟 bean。

Quarkus supports the use of mock objects using two different approaches. You can either use CDI alternatives to mock out a bean for all test classes, or use QuarkusMock to mock out beans on a per test basis.

CDI @Alternative mechanism.

要使用此功能,只需使用 src/test/java 目录中的类来覆盖你想要模拟的 bean,并在 bean 上放置 @Alternative@Priority(1) 注释,或者,可以使用一个方便的 io.quarkus.test.Mock 抽象注释,内置的此抽象注释声明 @Alternative@Priority(1)@Dependent,例如,如果我有以下服务:

To use this simply override the bean you wish to mock with a class in the src/test/java directory, and put the @Alternative and @Priority(1) annotations on the bean. Alternatively, a convenient io.quarkus.test.Mock stereotype annotation could be used. This built-in stereotype declares @Alternative, @Priority(1) and @Dependent. For example if I have the following service:

@ApplicationScoped
public class ExternalService {

    public String service() {
        return "external";
    }

}

我可以在 src/test/java 中使用以下类对其进行模拟:

I could mock it with the following class in src/test/java:

@Mock
@ApplicationScoped (1)
public class MockExternalService extends ExternalService {

    @Override
    public String service() {
        return "mock";
    }
}
1 Overrides the @Dependent scope declared on the @Mock stereotype.

替代项出现在 src/test/java 目录而不是 src/main/java 中非常重要,因为否则它将始终生效,而不仅仅是在测试时。

It is important that the alternative be present in the src/test/java directory rather than src/main/java, as otherwise it will take effect all the time, not just when testing.

请注意,目前此方法不适用于本机图像测试,因为这需要将测试备选方案编译到本机图像中。

Note that at present this approach does not work with native image testing, as this would require the test alternatives to be baked into the native image.

Mocking using QuarkusMock

`io.quarkus.test.junit.QuarkusMock`类可以用来临时模拟任何普通作用域的 bean。如果你在 `@BeforeAll`方法中使用此方法,模拟将对当前类的所有测试生效,如果你在测试方法中使用此方法,则模拟将仅对当前测试的持续时间有效。

The io.quarkus.test.junit.QuarkusMock class can be used to temporarily mock out any normal scoped bean. If you use this method in a @BeforeAll method the mock will take effect for all tests on the current class, while if you use this in a test method the mock will only take effect for the duration of the current test.

此方法可用于任何正常的范围 CDI bean(例如 @ApplicationScoped,`@RequestScoped`等,基本上除了 `@Singleton`和 `@Dependent`之外的所有范围)。

This method can be used for any normal scoped CDI bean (e.g. @ApplicationScoped, @RequestScoped etc, basically every scope except @Singleton and @Dependent).

一个示例用法可能如下所示:

An example usage could look like:

@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 As the injected instance is not available here we use installMockForType, this mock is used for both test methods
2 We use installMockForInstance to replace the injected bean, this takes effect for the duration of the test method.

请注意,不依赖 Mockito,你可以使用任何你喜欢的模拟库,甚至可以手动重写对象以提供所需的行为。

Note that there is no dependency on Mockito, you can use any mocking library you like, or even manually override the objects to provide the behaviour you require.

使用 @Inject`会让你获得对所安装模拟实例的 CDI 代理,这不适用于传递给诸如 `Mockito.verify`等想要模拟实例本身的方法。因此,如果你需要调用诸如 `verify`等方法,你应该保留测试中的模拟实例,或使用 `@io.quarkus.test.InjectMock

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.

Further simplification with @InjectMock

在 `QuarkusMock`提供的功能的基础上,Quarkus 还允许用户毫不费力地利用 Mockito来模拟 `QuarkusMock`支持的 bean。

Building on the features provided by QuarkusMock, Quarkus also allows users to effortlessly take advantage of Mockito for mocking the beans supported by QuarkusMock.

此功能可用于带有 `@io.quarkus.test.InjectMock`注释 only if 的 `quarkus-junit5-mockito`依赖项:

This functionality is available with the @io.quarkus.test.InjectMock annotation only if the quarkus-junit5-mockito dependency is present:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>

使用 @InjectMock,可以按如下方式编写前面的示例:

Using @InjectMock, the previous example could be written as follows:

@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 The mockableBean1 is configured here for every test method of the class
3 Since the mockableBean2 mock has not been configured, it will return the default Mockito response.
4 In this test the mockableBean2 is configured, so it returns the configured response.

尽管上面的测试非常适合展示 `@InjectMock`的能力,但它并不能很好地代表一个真实的测试。在真实的测试中,我们很可能会配置一个模拟,然后测试一个使用模拟 bean 的 bean。这是一个例子:

Although the test above is good for showing the capabilities of @InjectMock, it is not a good representation of a real test. In a real test we would most likely configure a mock, but then test a bean that uses the mocked bean. Here is an example:

@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 Since we configured greetingService as a mock, the GreetingResource which uses the GreetingService bean, we get the mocked response instead of the response of the regular GreetingService bean

默认情况下,@InjectMock`注释可用于任何正常的 CDI 范围 bean(例如 `@ApplicationScoped@RequestScoped)。可以通过添加 `@MockitoConfig(convertScopes = true)`注释来模拟 `@Singleton`bean。这将为测试将 `@Singleton`bean 转换为 `@ApplicationScoped`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 作用域的后果时才应该执行此操作。

This is considered an advanced option and should only be performed if you fully understand the consequences of changing the scope of the bean.

Using Spies instead of Mocks with @InjectSpy

建立在 InjectMock 提供的功能之上,Quarkus 还允许用户毫不费力地利用 Mockito 监视受 QuarkusMock 支持的 bean。此功能通过 @io.quarkus.test.junit.mockito.InjectSpy 注解提供,该注解在 quarkus-junit5-mockito 依赖项中提供。

Building on the features provided by InjectMock, Quarkus also allows users to effortlessly take advantage of Mockito for spying on the beans supported by QuarkusMock. This functionality is available via the @io.quarkus.test.junit.mockito.InjectSpy annotation which is available in the quarkus-junit5-mockito dependency.

有时在测试中,您只需要验证采用了某个逻辑路径,或者只需要截取单个方法的响应,同时仍然执行 Spied 克隆上的其他方法。更多有关 Spy 部分模拟的详细信息,请参阅 Mockito documentation - Spying on real objects。在任一种情况下,更希望使用对象的 Spy。使用 @InjectSpy,前一个示例可以写成如下形式:

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 Instead of overriding the value, we just want to ensure that the greet method on our GreetingService was called by this test.
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 We are verifying that we get the mocked response from the Spy.

Using @InjectMock with @RestClient

@RegisterRestClient 在运行时注册 REST 客户端的实现,并且由于 bean 需要是常规作用域,因此您必须使用 @ApplicationScoped 对您的接口进行注解。

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();
}

对于测试类,这里有一个示例:

For the test class here is an example:

@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 Indicate that this injection point is meant to use an instance of RestClient.

Mocking with Panache

如果您正在使用 quarkus-hibernate-orm-panachequarkus-mongodb-panache 扩展,请查看 Hibernate ORM with Panache MockingMongoDB with Panache Mocking 文档,了解模拟数据访问的最简单方法。

If you are using the quarkus-hibernate-orm-panache or quarkus-mongodb-panache extensions, check out the Hibernate ORM with Panache Mocking and MongoDB with Panache Mocking documentation for the easiest way to mock your data access.

Testing Security

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

If you are using Quarkus Security, check out the Testing Security section for information on how to easily test security features of the application.

Starting services before the Quarkus application starts

在测试 Quarkus 应用程序启动之前启动 Quarkus 应用程序所依赖的一些服务是一个非常常见的需求。为了满足此需求,Quarkus 提供了 @io.quarkus.test.common.WithTestResourceio.quarkus.test.common.QuarkusTestResourceLifecycleManager

A very common need is to start some services on which your Quarkus application depends, before the Quarkus application starts for testing. To address this need, Quarkus provides @io.quarkus.test.common.WithTestResource and io.quarkus.test.common.QuarkusTestResourceLifecycleManager.

当使用 @WithTestResource 注解的测试时,Quarkus 将在测试之前运行相应的 QuarkusTestResourceLifecycleManager

When a test annotated with @WithTestResource, Quarkus will run the corresponding QuarkusTestResourceLifecycleManager before the test.

默认情况下,@WithTestResource 仅适用于对其放置注解的测试。使用 @WithTestResource 注解的每个测试都将导致重新扩充和重新启动应用程序(类似于在开发模式中检测到更改时发生的情况),以便纳入由注解配置的设置。这意味着如果在整个测试套件中使用了大量此注解,这些重新启动将会影响测试执行速度。

By default, @WithTestResource applies only to the test on which the annotation is placed. Each test that is annotated with @WithTestResource will result in the application being re-augmented and restarted (in a similar fashion as happens in dev-mode when a change is detected) in order to incorporate the settings configured by the annotation. This means that if there are many instances of the annotation used throughout the testsuite, test execution speed will be impacted by these restarts.

测试资源将用于给定的测试类或自定义配置文件。要激活所有测试,可以使用 @WithTestResource(restrictToAnnotatedClass = false)

Test resources are applied for a given test class or custom profile. To activate for all tests you can use @WithTestResource(restrictToAnnotatedClass = false).

当使用多个测试资源时,可以同时启动它们。为此,您需要设置 @WithTestResource(parallel = true)

When using multiple test resources they can be started concurrently. For that you need to set @WithTestResource(parallel = true).

Quarkus 提供了一些开箱即用的 QuarkusTestResourceLifecycleManager 实现(请参见启动 H2 数据库的 io.quarkus.test.h2.H2DatabaseTestResource 或启动模拟 Kubernetes API 服务器的 io.quarkus.test.kubernetes.client.KubernetesServerTestResource),但通常会创建自定义实现来满足特定应用程序需求。常见的情况包括使用 Testcontainers 启动 Docker 容器(可以在 here 中找到示例),或使用 Wiremock 启动模拟 HTTP 服务器(可以在 here 中找到示例)。

Quarkus provides a few implementations of QuarkusTestResourceLifecycleManager out of the box (see io.quarkus.test.h2.H2DatabaseTestResource which starts an H2 database, or io.quarkus.test.kubernetes.client.KubernetesServerTestResource which starts a mock Kubernetes API server), but it is common to create custom implementations to address specific application needs. Common cases include starting docker containers using Testcontainers (an example of which can be found here), or starting a mock HTTP server using Wiremock (an example of which can be found here).

由于 QuarkusTestResourceLifecycleManager 不是 CDI Bean,因此实现它的类不能使用 @Inject 注入字段。可以使用 String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class);

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);

Altering the test class

在创建需要将一些内容注入测试类的自定义 QuarkusTestResourceLifecycleManager 时,可以使用 inject 方法。例如,如果您有一个类似于以下内容的测试:

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
@WithTestResource(MyWireMockResource.class)
public class MyTest {

    @InjectWireMock // this a custom annotation you are defining in your own application
    WireMockServer wireMockServer;

    @Test
    public someTest() {
        // control wiremock in some way and perform test
    }
}

如以下代码片段的`inject`方法所示,可以通过让`MyWireMockResource`注入`wireMockServer`字段来完成:

Making MyWireMockResource inject the wireMockServer field can be done as shown in the inject method of the following code snippet:

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 对测试类执行任何必要的注入之后才会发生。

It is worth mentioning that this injection into the test class is not under the control of CDI and happens after CDI has performed any necessary injections into the test class.

Annotation-based test resources

可以使用注释启用并配置测试资源。这是通过在注释上放置`@WithTestResource`来实现的,该注释将用于启用并配置测试资源。

It is possible to write test resources that are enabled and configured using annotations. This is enabled by placing the @WithTestResource on an annotation which will be used to enable and configure the test resource.

例如,这定义了 @WithKubernetesTestServer`注释,您可以在测试中使用它来激活`KubernetesServerTestResource,但仅适用于带注释的测试类。您还可以将它们放在`QuarkusTestProfile`测试配置文件中。

For example, this defines the @WithKubernetesTestServer annotation, which you can use on your tests to activate the KubernetesServerTestResource, but only for the annotated test class. You can also place them on your QuarkusTestProfile test profiles.

@WithTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithKubernetesTestServer {
    /**
     * Start it with HTTPS
     */
    boolean https() default false;

    /**
     * Start it in CRUD mode
     */
    boolean crud() default true;

    /**
     * Port to use, defaults to any available port
     */
    int port() default 0;
}

`KubernetesServerTestResource`类必须实现`QuarkusTestResourceConfigurableLifecycleManager`接口才能使用前面的注释进行配置:

The KubernetesServerTestResource class has to implement the QuarkusTestResourceConfigurableLifecycleManager interface in order to be configured using the previous annotation:

public class KubernetesServerTestResource
        implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> {

    private boolean https = false;
    private boolean crud = true;
    private int port = 0;

    @Override
    public void init(WithKubernetesTestServer annotation) {
        this.https = annotation.https();
        this.crud = annotation.crud();
        this.port = annotation.port();
    }

    // ...
}

如果您要使注释可重复,则必须使用`@WithTestResourceRepeatable`对包含的注释类型进行注释。例如,这将定义一个可重复的`@WithRepeatableTestResource`注释。

If you want to make the annotation repeatable, the containing annotation type must be annotated with @WithTestResourceRepeatable. For example, this would define a repeatable @WithRepeatableTestResource annotation.

@WithTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(WithRepeatableTestResource.List.class)
public @interface WithRepeatableTestResource {

    String key() default "";

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @WithTestResourceRepeatable(WithRepeatableTestResource.class)
    @interface List {
        WithRepeatableTestResource[] value();
    }
}

Hang Detection

`@QuarkusTest`支持挂起检测,以帮助诊断任何意外挂起。如果在指定时间内没有取得任何进展(即未调用任何 JUnit 回调),则 Quarkus 将向控制台打印堆栈跟踪以帮助诊断挂起。此超时的默认值为 10 分钟。

@QuarkusTest has support for hang detection to help diagnose any unexpected hangs. If no progress is made for a specified time (i.e. no JUnit callbacks are invoked) then Quarkus will print a stack trace to the console to help diagnose the hang. The default value for this timeout is 10 minutes.

不会执行进一步的操作,测试将正常继续(通常持续到 CI 超时),但是打印的堆栈跟踪应该有助于诊断构建失败的原因。您可以使用`quarkus.test.hang-detection-timeout`系统属性控制此超时(您也可以在 application.properties 中设置此属性,但这在 Quarkus 启动之前不会被读取,因此 Quarkus 启动的超时将默认为 10 分钟)。

No further action will be taken, and the tests will continue as normal (generally until CI times out), however the printed stack traces should help diagnose why the build has failed. You can control this timeout with the quarkus.test.hang-detection-timeout system property (you can also set this in application.properties, but this won’t be read until Quarkus has started, so the timeout for Quarkus start will be the default of 10 minutes).

Native Executable Testing

也可以使用`@QuarkusIntegrationTest`测试本机可执行文件。这支持本指南中提到的所有功能,除了注入测试(并且本机可执行文件在单独的非 JVM 进程中运行,这实际上是不可能的)。

It is also possible to test native executables using @QuarkusIntegrationTest. This supports all the features mentioned in this guide except injecting into tests (and the native executable runs in a separate non-JVM process this is not really possible).

这在Native Executable Guide中有所涉及。

This is covered in the Native Executable Guide.

Using @QuarkusIntegrationTest

@QuarkusIntegrationTest 应该用于启动和测试 Quarkus 构建产生的工件,并且支持测试 jar(任何类型的)、本机映像或容器映像。简单来说,这意味着如果 Quarkus 构建 ({@s23} 或 gradle build) 的结果是 jar,则将启动该 jar 作为 java -jar …​ 并针对它运行测试。如果构建了本机映像,则应用程序将作为 ./application …​ 启动,并且再次针对正在运行的应用程序运行测试。最后,如果在构建期间(通过包含 quarkus-container-image-jib, quarkus-container-image-dockercontainer-image-podman 扩展名以及配置 quarkus.container-image.build=true 属性)创建了容器映像,则会创建一个容器并运行它(这需要存在可执行的 dockerpodman )。

@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.

由于使用 @QuarkusIntegrationTest`注释的测试会测试构建的结果,因此它应该作为集成测试套件的一部分运行 - 即,如果使用 Maven,则设置-DskipITs=false`;如果使用 Gradle,则设置`quarkusIntTest`任务。如果与 `@QuarkusTest`在同一阶段运行,则这些测试将*not*正常工作,因为 Quarkus 尚未创建最终产品。

As a test annotated with @QuarkusIntegrationTest tests the result of the build, it should be run as part of the integration test suite - i.e. by setting -DskipITs=false if using Maven or the quarkusIntTest task if using Gradle. These tests will not work if run in the same phase as @QuarkusTest as Quarkus has not yet created the final artifact.

`pom.xml`文件包含:

The pom.xml file contains:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                </systemPropertyVariables>
            </configuration>
        </execution>
    </executions>
</plugin>

这会指示 failsafe-maven-plugin 运行 integration-test。

This instructs the failsafe-maven-plugin to run integration-test.

然后,打开`src/test/java/org/acme/quickstart/GreetingResourceIT.java`。它包含:

Then, open the src/test/java/org/acme/quickstart/GreetingResourceIT.java. It contains:

package org.acme.quickstart;


import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest (1)
public class GreetingResourceIT extends GreetingResourceTest { (2)

    // Run the same tests

}
1 Use another test runner that starts the application from the native file before the tests. The executable is retrieved by the Failsafe Maven Plugin.
2 We extend our previous tests as a convenience, but you can also implement your tests.

可以在 Testing the native executable Guide中找到更多信息。

More information can be found in the Testing the native executable Guide.

使用 @QuarkusIntegrationTest 进行应用程序测试时,将使用 prod 配置文件启动该应用程序,不过,可以通过使用 quarkus.test.integration-test-profile 属性更改此设置。

When the application is tested using @QuarkusIntegrationTest it is launched using the prod configuration profile, but this can be changed using the quarkus.test.integration-test-profile property.

在单元测试中 src/test/resources/application.properties 允许添加针对测试特定配置属性(注意此处是 test,而不是 main),但在集成测试中不允许此操作。

While adding test-specific configuration properties using src/test/resources/application.properties (note there’s test, not main) is possible for unit tests, it’s not possible for integration tests.

Launching containers

@QuarkusIntegrationTest 导致启动容器(因为在构建应用程序时将 quarkus.container-image.build 设置为 true),该容器将在可预测的容器网络中启动。这能简化编写需要启动服务支持应用程序的集成测试。这表示 @QuarkusIntegrationTest 完全适用于通过 Dev Services 启动的容器,但这也意味着它允许使用 QuarkusTestLifecycleManager 资源,这些资源可启动附加容器。可以通过让 QuarkusTestLifecycleManager 实现 io.quarkus.test.common.DevServicesContext.ContextAware 来实现这一点。一个简单的示例可能是以下示例:

When @QuarkusIntegrationTest results in launching a container (because the application was built with quarkus.container-image.build set to true), the container is launched on a predictable container network. This facilitates writing integration tests that need to launch services to support the application. This means that @QuarkusIntegrationTest works out of the box with containers launched via Dev Services, but it also means that it enables using quarkus-test-resource resources that launch additional containers. This can be achieved by having your QuarkusTestLifecycleManager implement io.quarkus.test.common.DevServicesContext.ContextAware. A simple example could be the following:

运行需要测试的资源的容器(例如通过 Testcontainers 的 PostgreSQL)将从容器的网络中分配一个 IP 地址。使用容器网络中的容器“公用”IP 以及“未映射”端口号连接到该服务。Testcontainers 库通常返回不符合容器网络的连接字符串,因此需要附加代码,以便通过容器网络上的容器 IP 和 unmapped 端口号向 Quarkus 提供“正确”连接字符串。

The container running the resource to test against, for example PostgreSQL via Testcontainers, is assigned an IP address from the container’s network. Use the container’s "public" IP from its network and the "unmapped" port number to connect to the service. The Testcontainers library usually return connection strings without respecting the container network, so additional code is needed to provide Quarkus the "correct" connection string using the container’s IP on the container network and the unmapped port number.

以下示例展示了与 PostgreSQL 一起使用,但该方法适用于所有容器。

The following example illustrates the use with PostgreSQL, but the approach is applicable to all containers.

import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class CustomResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {

    private Optional<String> containerNetworkId;
    private JdbcDatabaseContainer container;

    @Override
    public void setIntegrationTestContext(DevServicesContext context) {
        containerNetworkId = context.containerNetworkId();
    }

    @Override
    public Map<String, String> start() {
        // start a container making sure to call withNetworkMode() with the value of containerNetworkId if present
        container = new PostgreSQLContainer<>("postgres:latest").withLogConsumer(outputFrame -> {});

        // apply the network to the container
        containerNetworkId.ifPresent(container::withNetworkMode);

        // start container before retrieving its URL or other properties
        container.start();

        String jdbcUrl = container.getJdbcUrl();
        if (containerNetworkId.isPresent()) {
            // Replace hostname + port in the provided JDBC URL with the hostname of the Docker container
            // running PostgreSQL and the listening port.
            jdbcUrl = fixJdbcUrl(jdbcUrl);
        }

        // return a map containing the configuration the application needs to use the service
        return ImmutableMap.of(
            "quarkus.datasource.username", container.getUsername(),
            "quarkus.datasource.password", container.getPassword(),
            "quarkus.datasource.jdbc.url", jdbcUrl);
    }

    private String fixJdbcUrl(String jdbcUrl) {
        // Part of the JDBC URL to replace
        String hostPort = container.getHost() + ':' + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT);

        // Host/IP on the container network plus the unmapped port
        String networkHostPort =
            container.getCurrentContainerInfo().getConfig().getHostName()
            + ':'
            + PostgreSQLContainer.POSTGRESQL_PORT;

        return jdbcUrl.replace(hostPort, networkHostPort);
    }

    @Override
    public void stop() {
        // close container
    }
}

CustomResource 将使用 @WithTestResource@QuarkusIntegrationTest 中激活,如本文档的相应部分中所述。

CustomResource would be activated on a @QuarkusIntegrationTest using @WithTestResource as is described in the corresponding section of this doc.

Executing against a running application

@QuarkusIntegrationTest 支持对应用程序的已运行实例执行测试。在运行测试时,可以通过设置 quarkus.http.test-host 系统属性来实现此目的。

@QuarkusIntegrationTest supports executing tests against an already running instance of the application. This can be achieved by setting the quarkus.http.test-host system property when running the tests.

其示例用法可能是以下 Maven 命令,该命令强制 @QuarkusIntegrationTest 对可在 http://1.2.3.4:4321 访问的内容执行:

An example use of this could be the following Maven command, that forces @QuarkusIntegrationTest to execute against that is accessible at http://1.2.3.4:4321:

./mvnw verify -Dquarkus.http.test-host=1.2.3.4 -Dquarkus.http.test-port=4321

要针对仅接受 SSL/TLS 连接的运行实例进行测试(示例: https://1.2.3.4:4321),请将系统属性 quarkus.http.test-ssl-enabled 设置为 true

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.

Mixing @QuarkusTest with other type of tests

在单次执行运行(例如单次 Maven Surefire Plugin 执行)中,不允许将标有 @QuarkusTest 的测试与标有 @QuarkusDevModeTest@QuarkusProdModeTest@QuarkusUnitTest 的测试混合使用,而后三者可以共存。

Mixing tests annotated with @QuarkusTest with tests annotated with either @QuarkusDevModeTest, @QuarkusProdModeTest or @QuarkusUnitTest is not allowed in a single execution run (in a single Maven Surefire Plugin execution, for instance), while the latter three can coexist.

此限制的原因是, @QuarkusTest 会在测试执行运行的整个生命周期内启动 Quarkus 服务器,从而阻止其他测试启动自己的 Quarkus 服务器。

The reason of this restriction is that @QuarkusTest starts a Quarkus server for the whole lifetime of the tests execution run, thus preventing the other tests to start their own Quarkus server.

为消除此限制, @QuarkusTest 注释定义了一个 JUnit 5 @Tagio.quarkus.test.junit.QuarkusTest。您可以使用此标签在特定执行运行中隔离 @QuarkusTest 测试,例如使用 Maven Surefire Plugin:

To alleviate this restriction, the @QuarkusTest annotation defines a JUnit 5 @Tag: io.quarkus.test.junit.QuarkusTest. You can use this tag to isolate the @QuarkusTest test in a specific execution run, for example with the Maven Surefire Plugin:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
        <execution>
            <id>default-test</id>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <excludedGroups>io.quarkus.test.junit.QuarkusTest</excludedGroups>
            </configuration>
        </execution>
        <execution>
            <id>quarkus-test</id>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <groups>io.quarkus.test.junit.QuarkusTest</groups>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <systemProperties>
            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
        </systemProperties>
    </configuration>
</plugin>

Running @QuarkusTest from an IDE

大多数 IDE 都可直接作为 JUnit 测试运行所选类。为此,应在所选 IDE 设置中设置一些属性:

Most IDEs offer the possibility to run a selected class as a JUnit test directly. For this you should set a few properties in the settings of your chosen IDE:

  • java.util.logging.manager (see Logging Guide)

  • maven.home (only if there are any custom settings in ${maven.home}/conf/settings.xml, see Maven Guide)

  • maven.settings (in case a custom version of settings.xml file should be used for the tests)

Eclipse separate JRE definition

将当前的“已安装 JRE”定义复制到新的定义中,您将在其中添加属性作为新的 VM 参数:

Copy your current "Installed JRE" definition into a new one, where you will add the properties as a new VM arguments:

  • -Djava.util.logging.manager=org.jboss.logmanager.LogManager

  • -Dmaven.home=<path-to-your-maven-installation>

使用此 JRE 定义作为 Quarkus 项目的目标运行时,此替代方法将应用于任何“作为 JUnit 运行”配置。

Use this JRE definition as your Quarkus project targeted runtime and the workaround will be applied to any "Run as JUnit" configuration.

VSCode "run with" configuration

放置在项目目录根目录或工作区中的 settings.json 将在您的测试配置中需要以下替代方法:

The settings.json placed in the root of your project directory or in the workspace will need the following workaround in your test configuration:

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

IntelliJ IDEA JUnit template

在 IntelliJ IDEA 中无需任何操作,因为该 IDE 会从 pom.xml 中的 surefire 插件配置中选取 systemPropertyVariables

Nothing needed in IntelliJ IDEA because the IDE will pick the systemPropertyVariables from the surefire plugin configuration in pom.xml.

Testing Dev Services

默认情况下,测试应该通过 Dev Services正常工作,但是从某些用例中你可能需要访问测试中的自动配置属性。

By default, tests should just work with Dev Services, however from some use cases you may need access to the automatically configured properties in your tests.

您可以通过 io.quarkus.test.common.DevServicesContext 来实现,可以直接将 io.quarkus.test.common.DevServicesContext 注入任何 @QuarkusTest@QuarkusIntegrationTest。您需要做的就是定义一个 DevServicesContext 类型的字段,它会自动注入。使用此字段您可以检索已设置的任何属性。通常这是用于直接从测试本身连接到资源,例如连接到 kafka 以向正在测试的应用程序发送消息。

You can do this with io.quarkus.test.common.DevServicesContext, which can be injected directly into any @QuarkusTest or @QuarkusIntegrationTest. All you need to do is define a field of type DevServicesContext and it will be automatically injected. Using this you can retrieve any properties that have been set. Generally this is used to directly connect to a resource from the test itself, e.g. to connect to kafka to send messages to the application under test.

还支持将依赖项注入到实现 `io.quarkus.test.common.DevServicesContext.ContextAware`的对象中。如果你具有一个实现 `io.quarkus.test.common.DevServicesContext.ContextAware`的字段,Quarkus 将调用 `setIntegrationTestContext`方法以将上下文传递到该对象中。这允许客户端逻辑被封装在实用程序类中。

Injection is also supported into objects that implement io.quarkus.test.common.DevServicesContext.ContextAware. If you have a field that implements io.quarkus.test.common.DevServicesContext.ContextAware Quarkus will call the setIntegrationTestContext method to pass the context into this object. This allows client logic to be encapsulated in a utility class.

`QuarkusTestResourceLifecycleManager`实现还可以实现 `ContextAware`以获取对这些属性的访问权限,这允许你在 Quarkus 启动之前设置资源(例如,配置 KeyCloak 实例、将数据添加到数据库等)。

QuarkusTestResourceLifecycleManager implementations can also implement ContextAware to get access to these properties, which allows you to set up the resource before Quarkus starts (e.g. configure a KeyCloak instance, add data to a database etc).

对于 `@QuarkusIntegrationTest`测试,结果会将应用作为一个容器启动,`io.quarkus.test.common.DevServicesContext`还提供对该容器网络 ID 的访问权限,应用容器在此网络上启动(通过 `containerNetworkId`方法)。`QuarkusTestResourceLifecycleManager`可以使用此方法来启动应用需要与之通信的其他容器。

For @QuarkusIntegrationTest tests that result in launcher the application as a container, io.quarkus.test.common.DevServicesContext also provides access to the id of the container network on which the application container was launched (via the containerNetworkId method). This can be used by QuarkusTestResourceLifecycleManager that need to launch additional containers that the application will communicate with.

Testing Components

Quarkus 提供 QuarkusComponentTestExtension,一个 JUnit 扩展,该扩展可简化组件的测试及其依赖项的模拟。这个 JUnit 扩展在 `quarkus-junit5-component`依赖项中可用。

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.

我们来看一个组件 Foo——一个包含两个注入点的 CDI Bean。

Let’s have a component Foo - a CDI bean with two injection points.

Foo component
package org.acme;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped 1
public class Foo {

    @Inject
    Charlie charlie; 2

    @ConfigProperty(name = "bar")
    boolean bar; 3

    public String ping() {
        return bar ? charlie.ping() : "nok";
    }
}
1 Foo 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:

Simple component test
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 unsatisifed 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.

你可以在 testing components reference guide中找到更多示例和提示。

You can find more examples and hints in the testing components reference guide.