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 17+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.9.6

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

  • 来自 《入门指南》 的完整的greeter应用程序

2. 架构

在本指南中,我们对作为入门指南的一部分而创建的初始测试进行扩展。我们涵盖了注入测试以及如何测试本地可执行文件。

Quarkus支持持续测试,但这是 持续测试指南 所讨论的。

3. 解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或者下载 存档

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

本指南假设您已经准备好了 getting-started 目录中的应用。

4. 对JVM模式中基于HTTP的测试的回顾

如果您已从入门的例子开始,那您应该已经有了一个完成的测试例子,包括正确的安装了工具。

在您的构建文件中应该有2个测试依赖:

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. 测试特定的节点

Both RESTassured and @TestHTTPResource allow you to specify the endpoint class you are testing rather than hard coding a path. This currently supports both Jakarta REST endpoints, Servlets and Reactive Routes. This makes it a lot easier to see exactly which endpoints a given test is testing.

为了演示这些例子的目的,这里将假设我们有一个类似于下面的节点:

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

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}
This currently does not support the @ApplicationPath() annotation to set the Jakarta REST context path. Use the quarkus.resteasy.path config value instead if you want a custom context path.

5.1. TestHTTPResource

您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint 注释来指定节点路径,该路径会从提供的节点中提取。如果您还为 TestHTTPResource 节点指定了另外的值,它将被附加到节点路径后。

package org.acme.getting.started.testing;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class StaticContentTest {

    @TestHTTPEndpoint(GreetingResource.class)  (1)
    @TestHTTPResource
    URL url;

    @Test
    public void testIndexHtml() throws IOException {
        try (InputStream in = url.openStream()) {
            String contents = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            Assertions.assertEquals("hello", contents);
        }
    }
}
1 因为 GreetingResource 用到了注解 @Path("/hello") ,注入的URL会以 /hello 结束。

5.2. RESTassured

为了控制RESTassured基础路径(即作为每个请求根路径的默认路径),您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint 注解。可以在类或方法层面上使用。为了测试greeting资源类,我们可以这样做:

package org.acme.getting.started.testing;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
@TestHTTPEndpoint(GreetingResource.class) (1)
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        when().get()    (2)
          .then()
             .statusCode(200)
             .body(is("hello"));
    }
}
1 这将使RESTAssured在所有请求之前加上 /hello
2 注意我们不需要在此指定路径,因为 /hello 是这个测试的默认路径

6. 注入测试

到目前为止,我们只涉及了通过HTTP节点测试应用程序的集成式测试,但如果我们想做单元测试并直接测试我们的Bean又该怎么办?

Quarkus支持这种方式。它允许使用 @Inject 注解来将CDI Bean注入您的测试中(事实上,Quarkus中的测试是完整的CDI Bean,所以您可以使用所有的CDI功能)。让我们创建一个简单的测试,直接测试greeting服务,而非使用HTTP:

package org.acme.getting.started.testing;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class GreetingServiceTest {

    @Inject (1)
    GreetingService service;

    @Test
    public void testGreetingService() {
        Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));
    }
}
1 GreetingService bean将被注入到测试类中

7. 在测试中使用拦截器

如上所述,Quarkus测试实际上是完整的CDI Bean,因此您可以自由的使用CDI拦截器。举个例子,如果您想让测试方法在事务上下文中运行,您可以将 @Transactional 注解加到该方法上,之后事务拦截器将会处理。

除此以外,您还可以创建您自己的测试 stereotypes。例如,我们可以创建一个 @TransactionalQuarkusTest ,如下所示:

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

如果我们将这个注解应用于测试类,那它就类似于我们同时应用了 @QuarkusTest@Transactional 注解一样,例如:

@TransactionalQuarkusTest
public class TestStereotypeTestCase {

    @Inject
    UserTransaction userTransaction;

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

}

8. 测试与事务

您可以在测试中使用标准的Quarkus @Transactional 注解,但这意味着您的测试对数据库的改变将是持久的。如果您想在测试结束时回滚所做的任何改变,您可以使用 io.quarkus.test.TestTransaction 注释。这将在一个事务中运行测试方法,但一旦测试方法完成,事务就会回滚从而以恢复所有数据库变化。

9. 通过QuarkusTest*Callback来增强

作为拦截器的替代或补充,您可以通过实现以下回调接口来增强 所有@QuarkusTest 类:

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

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

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

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

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

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

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

这种回调实现必须作为java "服务提供者(service provider) "注册,从而被 java.util.ServiceLoader 来加载。

例如下面的回调例子:

package org.acme.getting.started.testing;

import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;

public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {

    @Override
    public void beforeEach(QuarkusTestMethodContext context) {
        System.out.println("Executing " + context.getTestMethod());
    }
}

该回调类必须通过 src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback 注册,具体如下:

org.acme.getting.started.testing.MyQuarkusTestBeforeEachCallback
可以从测试类或方法中读取注解,从而控制回调应做什么。
While it is possible to use JUnit Jupiter callback interfaces like BeforeEachCallback, you might run into classloading issues because Quarkus has to run tests in a custom classloader which JUnit is not aware of.

10. 测试不同的Profiles

到目前为止,在我们所有的例子中,我们只为所有的测试启动Quarkus一次。在第一个测试运行之前,Quarkus会启动,然后所有测试都会在这次启动中运行,最后Quarkus会关闭。这使得测试体验非常快,但是它有一点局限性,因为您无法测试不同的配置。

为了解决这个问题,Quarkus支持测试profile。如果一个测试有一个与之前运行的测试不同的profile,那么Quarkus将被停止,并在运行对应的测试之前使用新的profile启动。这显然会有些许变慢,因为它在测试时间上增加了一个停止/启动周期,但同时也提供了很大的灵活性。

为了减少Quarkus需要重启的次数, io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer 被注册为全局 ClassOrderer ,如 JUnit 5用户指南 中所述。这个orderer的行为可以通过 junit-platform.properties (更多细节见源代码或javadoc)来配置。它也可以通过配置另一个由JUnit 5所提供的ClassOrderer,甚至是您自己的自定义Orderer来完全禁用。+ 请注意,从JUnit 5.8.2开始 只能有一个 junit-platform.properties 被使用,如果超过一个就会有警告记录 。如果您遇到这样的警告,您可以通过从classpath中移除Quarkus提供的 junit-platform.properties 来删除它们:

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

10.1. 编写Profile

为了实现一个测试profile,我们需要实现 io.quarkus.test.junit.QuarkusTestProfile

package org.acme.getting.started.testing;

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

import jakarta.enterprise.inject.Produces;

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

public class MockGreetingProfile implements QuarkusTestProfile { (1)

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

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

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

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


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

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

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

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

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

    @Produces (2)
    public ExternalService mockExternalService() {
       return new ExternalService("mock");
    }
}
1 所有这些方法都有默认的实现,所以只要覆盖您需要覆盖的方法就可以。
2 If a test profile implementation declares a CDI bean (via producer method/field or nested static class) then this bean is only taken into account if the test profile is used, i.e. it’s ignored for any other test profile.

现在我们已经定义了我们自己的profile,我们需要在我们的测试类中引入它。我们通过在测试类中注解 @TestProfile(MockGreetingProfile.class) 来做到这一点。

所有的测试profile配置都存储在一个单一的类中,这使得我们很容易知道之前的测试是否以相同的配置运行。

10.2. 运行特定的测试

Quarkus提供了将测试限制在具有特定 @TestProfile 注释的测试中执行的能力。这是通过联合利用 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 Set.of("test1");
        }
    }

    public static class MultipleTags implements QuarkusTestProfile {
        @Override
        public Set<String> tags() {
            return Set.of("test1", "test2");
        }
    }
}

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

@QuarkusTest
public class NoQuarkusProfileTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.NoTags.class)
public class NoTagsTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.SingleTag.class)
public class SingleTagTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.MultipleTags.class)
public class MultipleTagsTest {

    @Test
    public void test() {
        // test something
    }
}

让我们考虑以下场景:

  • quarkus.test.profile.tags 未被设置。所有的测试都将被执行。

  • quarkus.test.profile.tags=foo :在这种情况下,所欧测试都不会被执行,因为在 QuarkusTestProfile 实现上定义的标签中没有一个与 quarkus.test.profile.tags 的值相匹配。注意, NoQuarkusProfileTest 也不会被执行,因为它没有 @TestProfile 注解。

  • quarkus.test.profile.tags=test1 :在这种情况下, 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库,甚至可以手动覆盖对象以提供您需要的行为。

Using @Inject will get you a CDI proxy to the mock instance you install, which is not suitable for passing to methods such as Mockito.verify which want the mock instance itself. So if you need to call methods such as verify you should hang on to the mock instance in your test, or use @io.quarkus.test.InjectMock.

11.2.1. 使用 @InjectMock 进行进一步简化

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

This functionality is available with the @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 ,前面的例子可以写成下面形式:

@QuarkusTest
public class MockTestCase {

    @InjectMock
    MockableBean1 mockableBean1; (1)

    @InjectMock
    MockableBean2 mockableBean2;

    @BeforeEach
    public void setup() {
        Mockito.when(mockableBean1.greet("Stuart")).thenReturn("A mock for Stuart"); (2)
    }

    @Test
    public void firstTest() {
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals(null, mockableBean2.greet("Stuart")); (3)
    }

    @Test
    public void secondTest() {
        Mockito.when(mockableBean2.greet("Stuart")).thenReturn("Bonjour Stuart"); (4)
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
    }

    @ApplicationScoped
    public static class MockableBean1 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }

    @ApplicationScoped
    public static class MockableBean2 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }
}
1 @InjectMock results in a Mockito mock being created, which is then available in test methods of the test class (other test classes are not affected by this)
2 mockableBean1 被mockito配置并可以为类的每个测试方法所用
3 由于 mockableBean2 mock还没有被配置,它将返回默认的Mockito响应。
4 在这个测试中, mockableBean2 进行了配置,所以它返回配置好的响应。

尽管上面的测试很好地展示了 @InjectMock 的能力,但它并不能很好地表示一个真实的测试案例。在一个真实的测试案例中,我们很可能会配置一个mock,然后测试一个使用了mocked Bean的Bean。下面是一个例子:

@QuarkusTest
public class MockGreetingServiceTest {

    @InjectMock
    GreetingService greetingService;

    @Test
    public void testGreeting() {
        when(greetingService.greet()).thenReturn("hi");
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hi")); (1)
    }

    @Path("greeting")
    public static class GreetingResource {

        final GreetingService greetingService;

        public GreetingResource(GreetingService greetingService) {
            this.greetingService = greetingService;
        }

        @GET
        @Produces("text/plain")
        public String greet() {
            return greetingService.greet();
        }
    }

    @ApplicationScoped
    public static class GreetingService {
        public String greet(){
            return "hello";
        }
    }
}
1 由于我们将 greetingService 配置为一个mock,那么在使用了 GreetingService Bean的 GreetingResource 中,我们得到的是mock的响应,而不是正常的 GreetingService Bean的响应

By default, the @InjectMock annotation can be used for any normal CDI scoped bean (e.g. @ApplicationScoped, @RequestScoped). Mocking @Singleton beans can be performed by adding the @MockitoConfig(convertScopes = true) annotation. This will convert the @Singleton bean to an @ApplicationScoped bean for the test.

这是一个高级选项,只有在您完全了解改变Bean scope的后果时才可以执行。

11.2.2. 通过 @InjectSpy 用Spies来代替Mocks

基于 InjectMock 所提供的功能,Quarkus还允许用户很容易利用 Mockito 来对 QuarkusMock 所支持的bean进行spy操作。这个功能可以通过 @io.quarkus.test.junit.mockito.InjectSpy 注解来实现,该注解通过 quarkus-junit5-mockito 依赖导入。

Sometimes when testing you only need to verify that a certain logical path was taken, or you only need to stub out a single method’s response while still executing the rest of the methods on the Spied clone. Please see Mockito documentation - Spying on real objects for more details on Spy partial mocks. In either of those situations a Spy of the object is preferable. Using @InjectSpy, the previous example could be written as follows:

@QuarkusTest
public class SpyGreetingServiceTest {

    @InjectSpy
    GreetingService greetingService;

    @Test
    public void testDefaultGreeting() {
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hello"));

        Mockito.verify(greetingService, Mockito.times(1)).greet(); (1)
    }

    @Test
    public void testOverrideGreeting() {
        doReturn("hi").when(greetingService).greet(); (2)
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hi")); (3)
    }

    @Path("greeting")
    public static class GreetingResource {

        final GreetingService greetingService;

        public GreetingResource(GreetingService greetingService) {
            this.greetingService = greetingService;
        }

        @GET
        @Produces("text/plain")
        public String greet() {
            return greetingService.greet();
        }
    }

    @ApplicationScoped
    public static class GreetingService {
        public String greet(){
            return "hello";
        }
    }
}
1 我们没有覆盖这个值,而只是想确保我们的 GreetingService 上的greet方法被这个测试所调用。
2 Here we are telling the Spy to return "hi" instead of "hello". When the GreetingResource requests the greeting from GreetingService we get the mocked response instead of the response of the regular GreetingService bean. Sometimes it’s impossible or impractical to use when(Object) for stubbing spies. Therefore when using spies please consider doReturn|Answer|Throw() family of methods for stubbing.
3 这里我们正在验证从Spy那里得到了mock响应。

11.2.3. 使用 @InjectMock@RestClient

The @RegisterRestClient registers the implementation of the REST Client at runtime, and because the bean needs to be a regular scope, you have to annotate your interface with @ApplicationScoped.

@Path("/")
@ApplicationScoped
@RegisterRestClient
public interface GreetingService {

    @GET
    @Path("/hello")
    @Produces(MediaType.TEXT_PLAIN)
    String hello();
}

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

@QuarkusTest
public class GreetingResourceTest {

    @InjectMock
    @RestClient (1)
    GreetingService greetingService;

    @Test
    public void testHelloEndpoint() {
        Mockito.when(greetingService.hello()).thenReturn("hello from mockito");

        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello from mockito"));
    }

}
1 这里表示这个注入点使用了 RestClient 的实例。

11.3. 使用Panache mock

如果您使用 quarkus-hibernate-orm-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服务器(例子 见这里 )。

As QuarkusTestResourceLifecycleManager is not a CDI Bean, classes that implement it can’t have fields injected with @Inject. You can use String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class);

13.1. 改动测试类

When creating a custom QuarkusTestResourceLifecycleManager that needs to inject something into the test class, the inject methods can be used. If for example you have a test like the following:

@QuarkusTest
@QuarkusTestResource(MyWireMockResource.class)
public class MyTest {

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

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

为了使 MyWireMockResource 注入 wireMockServer 字段,可按以下代码片断中的 inject 方法进行:

public class MyWireMockResource implements QuarkusTestResourceLifecycleManager {

    WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        wireMockServer = new WireMockServer(8090);
        wireMockServer.start();

        // create some stubs

        return Map.of("some.service.url", "localhost:" + wireMockServer.port());
    }

    @Override
    public synchronized void stop() {
        if (wireMockServer != null) {
            wireMockServer.stop();
            wireMockServer = null;
        }
    }

    @Override
    public void inject(TestInjector testInjector) {
        testInjector.injectIntoFields(wireMockServer, new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
    }
}
值得一提的是,这种对测试类的注入并不在CDI的控制之下,而是发生在CDI对测试类进行了所有必要的注入之后。

13.2. 基于注解的测试资源

我们也可以编写使用注解来启用和配置的测试资源。这可以通过在一个注解上使用 @QuarkusTestResource 来启用,该注解将被用来启用和配置测试资源。

例如,下面代码定义了 @WithKubernetesTestServer 注释,您可以在您的测试上使用它来激活 KubernetesServerTestResource ,但只针对被注释的测试类。您也可以把它们加入到您的 QuarkusTestProfile 测试profile中。

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

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

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

KubernetesServerTestResource 类必须实现 QuarkusTestResourceConfigurableLifecycleManager 接口,以便使用前面的注解进行配置:

public class KubernetesServerTestResource
        implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> {

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

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

    // ...
}

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

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

    String key() default "";

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

14. 挂起侦测

@QuarkusTest 支持挂起侦测,以帮助诊断任何意外的挂起。如果在指定的时间内没有进展(即没有调用JUnit回调),那么Quarkus将打印一个堆栈跟踪到控制台以帮助诊断挂起。这个超时的默认值是10分钟。

不会有进一步的动作执行,测试将继续正常进行(通常直到CI超时),但是打印出来的堆栈信息应该有助于诊断为什么构建失败了。您可以用 quarkus.test.hang-detection-timeout 系统属性来控制这个超时值(您也可以在application.properties中设置这个值,但是在Quarkus启动之前它不会被读取,所以Quarkus启动的默认超时时间将是10分钟)。

15. 本地可执行程序测试

您也可以使用 @QuarkusIntegrationTest 来测试本地可执行文件。除了注入测试(本地可执行文件在一个单独的非JVM进程中运行,所以它实际上是不可能做到的)以外,该特性支持本指南中提到的所有功能。

这在《 本地可执行文件指南》 中有所涉及。

16. 使用@QuarkusIntegrationTest

@QuarkusIntegrationTest 应该被用来启动和测试由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 可执行文件的存在)。

This is a black box test that supports the same set features and has the same limitations.

由于用 @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)

    // Run the same tests

}
1 这里使用另一个测试runner来通过本地文件在测试之前启动应用。该执行文件通过 Failsafe Maven Plugin 来获取。
2 为了方便起见,我们扩展了之前的测试,但您也可以实现您自己的测试。

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

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.

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.

16.1. 启动容器

@QuarkusIntegrationTest 启动一个容器时(因为应用程序在构建时将 quarkus.container-image.build 设置为 true ),该容器会在一个可预测的容器网络上启动。这有利于编写需要启动服务以支持应用程序的集成测试。这意味着 @QuarkusIntegrationTest 能够与通过 Dev Services 启动的容器一起开箱即用,但这也意味着它能够使用 QuarkusTestLifecycleManager 资源来启动额外的容器。这可以通过让您的 QuarkusTestLifecycleManager 实现 io.quarkus.test.common.DevServicesContext.ContextAware 来获得。一个简单的例子:

运行要测试资源的容器,例如通过Testcontainers启用的PostgreSQL,会从容器的网络中分配到一个IP地址。使用容器网络中的 "公共 "IP和 "未映射 "的端口号来连接到服务。Testcontainers库通常在不遵从容器网络规则的情况下返回连接字符串,所以需要额外的代码来为Quarkus提供 "正确的 "连接字符串,以使用容器网络中的IP和 未映射 的端口号。

下面的例子展示了在PostgreSQL上的使用,但这个方法也适用于所有的容器。

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

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

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

public class CustomResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {

    private Optional<String> containerNetworkId;
    private JdbcDatabaseContainer container;

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

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

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

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

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

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

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

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

        return jdbcUrl.replace(hostPort, networkHostPort);
    }

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

CustomResource 将在 @QuarkusIntegrationTest 上通过使用 @QuarkusTestResource 激活,就如本文档对应部分所述。

16.2. 对正在运行的应用程序执行测试

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

这方面的一个例子是下面的Maven命令,该命令强制 @QuarkusIntegrationTest 对位于 http://1.2.3.4:4321 节点执行测试 :

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

To test against a running instance that only accepts SSL/TLS connection (example: https://1.2.3.4:4321) set the system property quarkus.http.test-ssl-enabled to true.

17. 混合使用 @QuarkusTest 与其他类型的测试

在一次执行中(例如在一次Maven Surefire Plugin执行中),将注释为 @QuarkusTest 的测试与注释为 @QuarkusDevModeTest@QuarkusProdModeTest@QuarkusUnitTest 的测试混合执行是不被允许的,但是后三者可以共存。

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

为缓解这一限制, @QuarkusTest 注解定义了一个 JUnit 5 的 @Tag : io.quarkus.test.junit.QuarkusTest 。您可以使用该标记在特定的执行中隔离 @QuarkusTest 测试,例如使用Maven Surefire插件:

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

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 使用来启动一些应用程序需要通信的其他容器。

20. Testing Components

This feature is experimental and the API may change in the future.

In Quarkus, the component model is built on top CDI. Therefore, Quarkus provides the QuarkusComponentTestExtension, a JUnit extension to ease the testing of components and mocking of their dependencies. This extension is available in the quarkus-junit5-component dependency.

Let’s have a component Foo:

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.

Then a component test could look like:

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

@QuarkusComponentTest (1)
@TestConfigProperty(key = "bar", value = "true") (2)
public class FooTest {

    @Inject
    Foo foo; (3)

    @InjectMock
    Charlie charlieMock; (4)

    @Test
    public void testPing() {
        Mockito.when(charlieMock.ping()).thenReturn("OK"); (5)
        assertEquals("OK", foo.ping());
    }
}
1 The QuarkusComponentTest annotation registers the JUnit extension.
2 Sets a configuration property for the test.
3 The test injects the component under the test. The types of all fields annotated with @Inject are considered the component types under test. You can also specify additional component classes via @QuarkusComponentTest#value(). Furthermore, the static nested classes declared on the test class are components too.
4 The test also injects Charlie, a 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.

QuarkusComponentTestExtension also resolves parameters of test methods and injects matching beans. So the code snippet above can be rewritten as:

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
@TestConfigProperty(key = "bar", value = "true")
public class FooTest {

    @Test
    public void testPing(Foo foo, @InjectMock Charlie charlieMock) { (1)
        Mockito.when(charlieMock.ping()).thenReturn("OK");
        assertEquals("OK", foo.ping());
    }
}
1 Parameters annotated with @io.quarkus.test.component.SkipInject are never resolved by this extension.

Furthermore, if you need the full control over the QuarkusComponentTestExtension configuration then you can use the @RegisterExtension annotation and configure the extension programatically. The original test could be rewritten like:

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.QuarkusComponentTestExtension;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class FooTest {

    @RegisterExtension (1)
    static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder().configProperty("bar","true").build();

    @Inject
    Foo foo;

    @InjectMock
    Charlie charlieMock;

    @Test
    public void testPing() {
        Mockito.when(charlieMock.ping()).thenReturn("OK");
        assertEquals("OK", foo.ping());
    }
}
1 The QuarkusComponentTestExtension is configured in a static field of the test class.

20.1. Lifecycle

So what exactly does the QuarkusComponentTest do? It starts the CDI container and registers a dedicated configuration object. If the test instance lifecycle is Lifecycle#PER_METHOD (default) then the container is started during the before each test phase and stopped during the after each test phase. However, if the test instance lifecycle is Lifecycle#PER_CLASS then the container is started during the before all test phase and stopped during the after all test phase. The fields annotated with @Inject and @InjectMock are injected after a test instance is created. Finally, the CDI request context is activated and terminated per each test method.

20.2. Injection

Test class fields annotated with @jakarta.inject.Inject and @io.quarkus.test.InjectMock are injected after a test instance is created. Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed. Parameters of a test method for which a matching bean exists are resolved unless annotated with @io.quarkus.test.component.SkipInject. Dependent beans injected into the test method arguments are correctly destroyed after the test method completes.

Arguments of a @ParameterizedTest method that are provided by an ArgumentsProvider, for example with @org.junit.jupiter.params.provider.ValueArgumentsProvider, must be annotated with @SkipInject.

20.3. Auto Mocking Unsatisfied Dependencies

Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency. Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency. The bean has the @Singleton scope so it’s shared across all injection points with the same required type and qualifiers. The injected reference is an unconfigured Mockito mock. You can inject the mock in your test using the io.quarkus.test.InjectMock annotation and leverage the Mockito API to configure the behavior.

20.4. Custom Mocks For Unsatisfied Dependencies

Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. You can use the mock configurator API via the QuarkusComponentTestExtensionBuilder#mock() method.

20.5. Configuration

You can set the configuration properties for a test with the @io.quarkus.test.component.TestConfigProperty annotation or with the QuarkusComponentTestExtensionBuilder#configProperty(String, String) method. If you only need to use the default values for missing config properties, then the @QuarkusComponentTest#useDefaultConfigProperties() or QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties() might come in useful.

It is also possible to set configuration properties for a test method with the @io.quarkus.test.component.TestConfigProperty annotation. However, if the test instance lifecycle is Lifecycle#_PER_CLASS this annotation can only be used on the test class and is ignored on test methods.

CDI beans are also automatically registered for all injected Config Mappings. The mappings are populated with the test configuration properties.

20.6. Mocking CDI Interceptors

If a tested component class declares an interceptor binding then you might need to mock the interception too. There are two ways to accomplish this task. First, you can define an interceptor class as a static nested class of the test class.

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;

@QuarkusComponentTest
public class FooTest {

    @Inject
    Foo foo;

    @Test
    public void testPing() {
        assertEquals("OK", foo.ping());
    }

    @ApplicationScoped
    static class Foo {

       @SimpleBinding (1)
       String ping() {
         return "ok";
       }

    }

    @SimpleBinding
    @Interceptor
    static class SimpleInterceptor { (2)

        @AroundInvoke
        Object aroundInvoke(InvocationContext context) throws Exception {
            return context.proceed().toString().toUpperCase();
        }

    }
}
1 @SimpleBinding is an interceptor binding.
2 The interceptor class is automatically considered a tested component.
Static nested classed declared on a test class that is annotated with @QuarkusComponentTest are excluded from bean discovery when running a @QuarkusTest in order to prevent unintentional CDI conflicts.

Furthermore, you can also declare a "test interceptor method" directly on the test class. This method is then invoked in the relevant interception phase.

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;

@QuarkusComponentTest
public class FooTest {

    @Inject
    Foo foo;

    @Test
    public void testPing() {
        assertEquals("OK", foo.ping());
    }

    @SimpleBinding (1)
    @AroundInvoke (2)
    Object aroundInvoke(InvocationContext context) throws Exception {
       return context.proceed().toString().toUpperCase();
    }

    @ApplicationScoped
    static class Foo {

       @SimpleBinding (1)
       String ping() {
         return "ok";
       }

    }
}
1 The interceptor bindings of the resulting interceptor are specified by annotating the method with the interceptor binding types.
2 Defines the interception type.

Related content