编辑此页面

测试您的应用程序

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

  • JVM 模式下的测试

  • 原生模式下的测试

  • 将资源注入测试

1. 前提条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

  • 《入门指南》中已完成的 greeter 应用程序

2. 架构

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

Quarkus 支持持续测试,但这在《持续测试指南》中已涵盖。

3. 解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

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

解决方案位于 getting-started-testing 目录中。

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

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

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

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

Maven
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>
Gradle
dependencies {
    testImplementation("io.quarkus:quarkus-junit5")
    testImplementation("io.rest-assured:rest-assured")
}

quarkus-junit5 是测试所必需的,因为它提供了 @QuarkusTest 注释来控制测试框架。rest-assured 不是必需的,但它是测试 HTTP 端点的便捷方式,我们还提供了集成,可自动设置正确的 URL,因此无需进行配置。

由于我们使用 JUnit 5,因此必须设置 Surefire Maven Plugin 的版本,因为默认版本不支持 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。这允许您在应用程序并行运行时运行测试。

更改测试端口

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

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

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

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

4.2. 控制 HTTP 交互超时

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

quarkus.http.test-timeout=10s

4.3. 注入 URI

还可以直接将 URL 注入测试,这可以方便地使用不同的客户端。这通过 @TestHTTPResource 注释完成。

让我们编写一个简单的测试来演示加载一些静态资源。首先,在 src/main/resources/META-INF/resources/index.html 中创建一个简单的 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 此注释允许您直接注入 Quarkus 实例的 URL,注释的值将是 URL 的路径组件。

目前 @TestHTTPResource 允许您注入 URL 的 URIURLString 表示形式。

5. 测试特定端点

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

为了这些示例的目的,我将假定我们有一个如下所示的端点

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

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}
这目前不支持 @ApplicationPath() 注释来设置 Jakarta REST 上下文路径。如果需要自定义上下文路径,请改用 quarkus.resteasy.path 配置值。

5.1. TestHTTPResource

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

package org.acme.getting.started.testing;

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

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

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

@QuarkusTest
public class StaticContentTest {

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

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

5.2. RESTassured

要控制 RESTassured 的基本路径(即作为每个请求根的默认路径),您可以使用 io.quarkus.test.common.http.TestHTTPEndpoint 注释。这可以应用于类或方法级别。为了测试 greeting 资源,我们将这样做

package org.acme.getting.started.testing;

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

import java.util.UUID;

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

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

    @Test
    public void testHelloEndpoint() {
        when().get()    (2)
          .then()
             .statusCode(200)
             .body(is("hello"));
    }
}
1 这告诉 RESTAssured 将所有请求都以 /hello 作为前缀。
2 请注意,我们无需在此处指定路径,因为 /hello 是此测试的默认值。

6. 将内容注入测试

到目前为止,我们只涵盖了通过 HTTP 端点测试应用程序的集成式测试,但是如果我们想进行单元测试并直接测试我们的 bean 怎么办?

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

package org.acme.getting.started.testing;

import 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 将被注入到测试中。
如果您想注入/测试一个 @SessionScoped bean,那么会话上下文很可能不处于活动状态,并且在调用注入 bean 的方法时,您会收到 ContextNotActiveException。但是,可以使用 @io.quarkus.test.ActivateSessionContext 拦截器绑定来为特定业务方法激活会话上下文。请阅读 javadoc 以了解进一步的限制。

7. 将拦截器应用于测试

如上所述,Quarkus 测试实际上是完整的 CDI bean,因此您可以像平常一样应用 CDI 拦截器。例如,如果您希望某个测试方法在事务上下文中运行,您可以简单地将 @Transactional 注释应用于该方法,事务拦截器将负责处理它。

此外,您还可以创建自己的测试注解。例如,我们可以创建一个 @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.QuarkusTestBeforeTestExecutionCallback

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

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

可选地,如果属性 quarkus.test.enable-callbacks-for-integration-teststrue,您也可以为 @QuarkusIntegrationTest 测试启用这些回调。

此类回调实现必须注册为 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
可以读取测试类或方法的注释来控制回调的操作。

10. 测试不同的配置文件

到目前为止,在我们所有的示例中,我们只为所有测试启动一次 Quarkus。在运行第一个测试之前,Quarkus 将启动,然后所有测试将运行,最后 Quarkus 将在结束时关闭。这提供了非常快速的测试体验,但它有些有限,因为您无法测试不同的配置。

为了解决这个问题,Quarkus 支持测试配置文件的概念。如果一个测试的配置文件与之前运行的测试不同,那么 Quarkus 将会关闭并使用新配置文件启动,然后再运行测试。这显然会慢一些,因为它会在测试时间中添加一个关闭/启动周期,但提供了很大的灵活性。

为了减少 Quarkus 需要重新启动的次数,io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer 被注册为一个全局 ClassOrderer,如 JUnit 5 用户指南中所述。此 ClassOrderer 的行为可以通过 application.properties 使用属性 quarkus.test.class-orderer 进行配置。该属性接受 ClassOrderer 的 FQCN。如果找不到该类,它将回退到 JUnit 的默认行为,即根本不设置 ClassOrderer。您也可以通过设置 JUnit 5 提供的另一个 ClassOrderer 或您自己的自定义 ClassOrderer 来完全禁用它。

10.1. 编写配置文件

要实现测试配置文件,我们需要实现 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 如果测试配置文件实现声明了一个 CDI bean(通过生产者方法/字段或嵌套静态类),那么该 bean 仅在该测试配置文件使用时才会被考虑,也就是说,它会被任何其他测试配置文件忽略。

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

所有测试配置都存储在一个类中,这使得很容易判断之前的测试是否使用了相同的配置。

10.2. 运行特定测试

Quarkus 提供了将测试执行限制为具有特定 @TestProfile 注释的测试的能力。这通过将 QuarkusTestProfiletags 方法与 quarkus.test.profile.tags 系统属性结合使用来实现。

本质上,任何具有至少一个与 quarkus.test.profile.tags 值匹配的标签的 QuarkusTestProfile 都将被视为活动状态,而所有用 @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 是唯一一个 QuarkusTestProfile 实现的 tags 方法与 quarkus.test.profile.tags 的值匹配。

11. 嵌套测试

JUnit 5 @Nested tests 对于构建更复杂的测试场景很有用。但是,请注意,无法在同一父类中为嵌套测试分配不同的测试配置文件或资源。

12. Mock 支持

Quarkus 支持使用两种不同的方法使用 mock 对象。您可以覆盖 bean 以 mockout 所有测试类的 bean,或者使用 QuarkusMock 以每个测试为基础 mockout bean。

12.1. CDI @Alternative 机制。

要使用此方法,只需将要 mock 的 bean 替换为 src/test/java 目录中的类,并在 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 范围。

重要的是,替代项必须存在于 src/test/java 目录中,而不是 src/main/java 中,否则它将一直生效,而不仅仅是在测试时。

请注意,此方法目前不适用于原生映像测试,因为这需要将测试替代项烘焙到原生映像中。

12.2. 使用 QuarkusMock 进行 Mock

io.quarkus.test.junit.QuarkusMock 类可用于临时 mock 任何正常范围的 bean。如果您在 @BeforeAll 方法中使用此方法,则 mock 将在当前类的所有测试中生效,而如果您在测试方法中使用此方法,则 mock 将仅在当前测试期间生效。

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

使用示例可能如下:

@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 的依赖项,您可以使用任何您喜欢的 mock 库,甚至可以手动覆盖对象以提供所需行为。

使用 @Inject 将获得 mock 实例的 CDI 代理,这不适合传递给 Mockito.verify 等方法,因为这些方法需要 mock 实例本身。因此,如果您需要调用 verify 等方法,您应该在测试中保留 mock 实例,或使用 @io.quarkus.test.InjectMock

12.2.1. 使用 @InjectMock 进一步简化

基于 QuarkusMock 提供的功能,Quarkus 还允许用户轻松利用 Mockito 来 mock QuarkusMock 支持的 bean。

此功能可在 @io.quarkus.test.InjectMock 注释中获得,前提是 存在 quarkus-junit5-mockito 依赖项。

<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 会创建一个 Mockito mock,该 mock 可在测试类的测试方法中使用(其他测试类不受此影响)。
2 mockableBean1 在此处为类的所有测试方法配置。
3 由于 mockableBean2 mock 未配置,它将返回默认的 Mockito 响应。
4 在此测试中,mockableBean2 已配置,因此它返回配置的响应。

虽然上面的测试很好地展示了 @InjectMock 的功能,但它并不是实际测试的良好表示。在实际测试中,我们很可能会配置一个 mock,然后测试使用被 mock bean 的 bean。下面是一个示例:

@QuarkusTest
public class MockGreetingServiceTest {

    @InjectMock
    GreetingService greetingService;

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

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

        final GreetingService greetingService;

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

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

    @ApplicationScoped
    public static class GreetingService {
        public String greet(){
            return "hello";
        }
    }
}
1 由于我们将 greetingService 配置为 mock,因此使用 GreetingService bean 的 GreetingResource 将获得被 mock 的响应,而不是常规 GreetingService bean 的响应。

默认情况下,@InjectMock 注释可用于任何正常范围的 CDI bean(例如 @ApplicationScoped@RequestScoped)。通过添加 @MockitoConfig(convertScopes = true) 注释,可以 mock @Singleton bean。这将把 @Singleton bean 转换为 @ApplicationScoped bean 以用于测试。

这被认为是一个高级选项,只有在您完全了解更改 bean 范围的后果时才应执行。

12.2.2. 使用 Spy 而不是 Mock,并使用 @InjectSpy

基于 InjectMock 提供的功能,Quarkus 还允许用户轻松利用 Mockito 来 spy QuarkusMock 支持的 bean。此功能通过 @io.quarkus.test.junit.mockito.InjectSpy 注释提供,该注释在 quarkus-junit5-mockito 依赖项中可用。

有时在测试中,您只需要验证某个逻辑路径是否被执行,或者只需要 stub out 单个方法的响应,同时仍然执行 Spy 克隆上其余方法的执行。有关 Spy 部分 mock 的更多详细信息,请参阅 Mockito 文档 - Spying on real objects。在这些情况中的任何一种情况下,Spy 对象都是首选。使用 @InjectSpy,前面的示例可以这样编写:

@QuarkusTest
public class SpyGreetingServiceTest {

    @InjectSpy
    GreetingService greetingService;

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

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

    @Test
    public void testOverrideGreeting() {
        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 在这里,我们告诉 Spy 返回“hi”而不是“hello”。当 GreetingResource 请求 GreetingService 的问候时,我们得到被 mock 的响应,而不是常规 GreetingService bean 的响应。有时使用 when(Object) stubbing spy 是不可能或不切实际的。因此,在使用 spy 时,请考虑使用 doReturn|Answer|Throw() 系列方法进行 stubbing。
3 我们正在验证我们是否从 Spy 获得被 mock 的响应。

12.2.3. 将 @InjectMock@RestClient 结合使用

@RegisterRestClient 在运行时注册 REST Client 的实现,并且由于 bean 需要是常规范围,因此您必须使用 @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 的实例。

12.3. 使用 Panache 进行 Mock

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

13. 测试安全

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

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

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

只需用 @QuarkusTestResource 注释测试套件中的任何测试,Quarkus 将在运行任何测试之前运行相应的 QuarkusTestResourceLifecycleManager。测试套件也可以自由使用多个 @QuarkusTestResource 注释,在这种情况下,所有相应的 QuarkusTestResourceLifecycleManager 对象将在测试运行之前运行。

测试资源是全局的,即使它们是在测试类或自定义配置文件上定义的,这意味着它们将对所有测试激活,即使我们删除了重复项。如果您只想在单个测试类或测试配置文件上启用测试资源,您可以使用 @QuarkusTestResource(restrictToAnnotatedClass = true)
当使用多个测试资源时,它们可以并行启动。为此,您需要设置 @QuarkusTestResource(parallel = true)

Quarkus 开箱即用地提供了 QuarkusTestResourceLifecycleManager 的一些实现(请参阅 io.quarkus.test.h2.H2DatabaseTestResource,它启动一个 H2 数据库,或 io.quarkus.test.kubernetes.client.KubernetesServerTestResource,它启动一个 mock Kubernetes API 服务器),但创建自定义实现以满足特定应用程序需求也很常见。常见情况包括使用 Testcontainers 启动 docker 容器(示例可在此处找到),或者使用 Wiremock 启动 mock HTTP 服务器(示例可在此处找到)。

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

14.1. 修改测试类

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

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

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

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

通过将 MyWireMockResource 注入 wireMockServer 字段,可以在以下代码片段的 inject 方法中完成:

public class MyWireMockResource implements QuarkusTestResourceLifecycleManager {

    WireMockServer wireMockServer;

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

        // create some stubs

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

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

    @Override
    public void inject(TestInjector testInjector) {
        testInjector.injectIntoFields(wireMockServer, new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
    }
}
值得一提的是,这种注入到测试类的过程不受 CDI 控制,而是在 CDI 完成对测试类的任何必要注入之后进行的。

14.2. 基于注释的测试资源

可以编写使用注释启用和配置的测试资源。这通过将 @QuarkusTestResource 放在将用于启用和配置测试资源的注释上来实现。

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

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

    // ...
}

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

@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.3. @WithTestResource 的用法

虽然 @QuarkusTestResource 提供的测试资源是全局可用的,或者限制在注释的测试类(restrictToAnnotatedClass)中,但 @WithTestResource 注释允许通过测试资源进一步对测试进行分组以进行执行。@WithTestResource 具有一个 scope 属性,该属性接受一个 TestResourceScope 枚举值。

  • TestResourceScope.MATCHING_RESOURCES(默认):Quarkus 将具有相同测试资源的测试分组并一起运行。执行完一个组后,所有测试资源都将停止,然后执行下一组。

  • TestResourceScope.RESTRICTED_TO_CLASS:测试资源仅适用于注释的测试类,并在测试类执行后停止。

  • TestResourceScope.GLOBAL:测试资源适用于测试套件中的所有测试。

Quarkus 需要重启,如果以下任一情况为真:

  • 当前测试的至少一个测试资源限制在测试类。

  • 下一个测试的至少一个测试资源限制在测试类。

  • 正在使用不同的 MATCHING_RESOURCES 范围的测试资源。

15. 挂起检测

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

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

16. 原生可执行文件测试

还可以使用 @QuarkusIntegrationTest 测试原生可执行文件。这支持本指南中提到的所有功能,但注入测试除外(原生可执行文件在单独的非 JVM 进程中运行,因此这实际上是不可能的)。

这在《构建原生映像指南》中已涵盖。

17. 使用 @QuarkusIntegrationTest

@QuarkusIntegrationTest 应用于启动和测试 Quarkus 构建生成的工件,并支持测试 jar(任何类型)、原生映像或容器映像。简而言之,这意味着如果 Quarkus 构建的结果(mvn packagegradle build)是一个 jar,那么该 jar 将作为 java -jar …​ 启动,并针对它运行测试。如果相反构建了原生映像,那么应用程序将作为 ./application …​ 启动,同样,测试也是针对正在运行的应用程序运行的。最后,如果在构建过程中创建了容器映像(通过包含 quarkus-container-image-jibquarkus-container-image-dockercontainer-image-podman 扩展并配置了 quarkus.container-image.build=true 属性),那么将创建并运行一个容器(这需要 dockerpodman 可执行文件)。

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

由于用 @QuarkusIntegrationTest 注释的测试是对构建结果进行测试,因此它应该在集成测试套件的一部分运行,即使用 Maven 时设置 -DskipITs=false 或使用 Gradle 时运行 quarkusIntTest 任务。这些测试在与 @QuarkusTest 相同的阶段运行不会成功,因为 Quarkus 尚未创建最终工件。

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 运行 integration-test。

然后,打开 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 使用另一个测试运行程序,该程序在测试之前从原生文件启动应用程序。可执行文件由Failsafe Maven Plugin检索。
2 为了方便起见,我们扩展了之前的测试,但您也可以实现自己的测试。

更多信息可以在《测试原生可执行文件指南》中找到。

当使用 @QuarkusIntegrationTest 测试应用程序时,它会使用 prod 配置文件的启动,但可以使用 quarkus.test.integration-test-profile 属性进行更改。

虽然使用 src/test/resources/application.properties(请注意是 test 而不是 main)添加特定于测试的配置属性对于单元测试是可行的,但对于集成测试则不行。

17.1. 启动容器

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

运行要测试的资源(例如,通过 Testcontainers 的 PostgreSQL)的容器从容器的网络中分配了一个 IP 地址。使用容器网络上的容器“公共”IP 和“未映射”端口号来连接服务。Testcontainers 库通常返回不考虑容器网络的连接字符串,因此需要额外的代码来使用容器网络上的容器 IP 和未映射的端口号为 Quarkus 提供“正确的”连接字符串。

以下示例说明了与 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 将使用 @QuarkusTestResource@QuarkusIntegrationTest 上激活,这已在本指南的相应部分进行了说明。

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

要测试仅接受 SSL/TLS 连接的正在运行的实例(例如:https://1.2.3.4:4321),请将系统属性 quarkus.http.test-ssl-enabled 设置为 true,并将 quarkus.http.test-ssl-port 设置为目标 HTTPS 端口。

18. 将 @QuarkusTest 与其他类型的测试混合

在单个执行运行(例如,在单个 Maven Surefire 插件执行中)中,不允许将用 @QuarkusTest 注释的测试与用 @QuarkusDevModeTest@QuarkusProdModeTest@QuarkusUnitTest 注释的测试混合,而最后三个可以共存。

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

为了减轻此限制,@QuarkusTest 注释定义了一个 JUnit 5 @Tagio.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>

目前 @QuarkusTest@QuarkusIntegrationTest 不应在同一测试运行中运行。

对于 Maven,这意味着前者应由 surefire 插件运行,而后者应由 failsafe 插件运行。

对于 Gradle,这意味着这两种类型的测试应属于不同的源集。

源集配置示例
/** custom source sets
 *
 * io.quarkus.gradle.QuarkusPlugin
 * io.quarkus.test.junit.IntegrationTestUtil
 *
 * to work around
 *  https://github.com/quarkusio/quarkus/issues/43796
 *  https://github.com/quarkusio/quarkus/issues/43804
 */

sourceSets {
    create("intTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
    create("e2eTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

configurations {
    getByName("intTestImplementation") {
        extendsFrom(configurations.testImplementation.get())
    }
    getByName("intTestRuntimeOnly") {
        extendsFrom(configurations.testRuntimeOnly.get())
    }
    getByName("e2eTestImplementation") {
        extendsFrom(configurations.testImplementation.get())
    }
    getByName("e2eTestRuntimeOnly") {
        extendsFrom(configurations.testRuntimeOnly.get())
    }
}

tasks.register<Test>("intTest") {
    group = "verification"
    description = "Runs integration tests"

    testClassesDirs = sourceSets["intTest"].output.classesDirs
    classpath = sourceSets["intTest"].runtimeClasspath

    systemProperty("build.output.directory", "build")
    systemProperty("quarkus.profile", "intTest")
}

tasks.register<Test>("e2eTest") {
    group = "verification"
    description = "Runs e2e tests"

    val quarkusBuild = tasks.getByName("quarkusBuild")
    dependsOn(quarkusBuild)

    testClassesDirs = sourceSets["e2eTest"].output.classesDirs
    classpath = sourceSets["e2eTest"].runtimeClasspath

    systemProperty("build.output.directory", "build")
}

idea.module {
    testSources.from(sourceSets["intTest"].kotlin.srcDirs)
    testSources.from(sourceSets["e2eTest"].kotlin.srcDirs)
}

19. 从 IDE 运行 @QuarkusTest

大多数 IDE 都提供直接将选定类作为 JUnit 测试运行的可能性。为此,您应该在所选 IDE 的设置中设置一些属性:

  • java.util.logging.manager(参见日志记录指南)。

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

  • maven.settings(以防万一要使用自定义版本的 settings.xml 文件进行测试)。

19.1. Eclipse 独立 JRE 定义

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

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

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

将此 JRE 定义用作 Quarkus 项目的目标运行时,并且该解决方法将应用于任何“Run as JUnit”配置。

19.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> ..." ],
        ...
    },
  ...
]

19.3. IntelliJ IDEA JUnit 模板

IntelliJ IDEA 中不需要任何内容,因为 IDE 将从 pom.xml 中的 surefire 插件配置中获取 systemPropertyVariables

20. 测试 Dev Services

默认情况下,测试应能与Dev Services正常工作,但出于某些用例的考虑,您可能需要在测试中访问自动配置的属性。

您可以通过 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 使用。

21. 测试组件

Quarkus 提供了 QuarkusComponentTestExtension,这是一个 JUnit 扩展,用于简化组件的测试以及其依赖项的 Mock。此 JUnit 扩展在 quarkus-junit5-component 依赖项中可用。

假设我们有一个组件 Foo - 一个带有两个注入点的 CDI bean。

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 是一个 @ApplicationScoped CDI bean。
2 Foo 依赖于 Charlie,后者声明了一个方法 ping()
3 Foo 依赖于配置属性 bar@Inject 不需要用于此注入点,因为它还声明了一个 CDI 限定符 - 这是 Quarkus 特有的功能。

那么组件测试可能看起来像:

简单的组件测试
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 QuarkusComponentTest 注释注册了 JUnit 扩展。
2 为测试设置配置属性。
3 测试注入被测试的组件。用 @Inject 注释的所有字段的类型被视为被测试的组件类型。您还可以通过 @QuarkusComponentTest#value() 指定其他组件类。此外,在测试类上声明的静态嵌套类也是组件。
4 测试还注入了 Charlie 的 mock。Charlie 是一个未满足的依赖项,为该依赖项自动注册了一个合成的 @Singleton bean。注入的引用是一个“未配置”的 Mockito mock。
5 我们可以在测试方法中利用 Mockito API 来配置行为。

您可以在《测试组件参考指南》中找到更多示例和提示。

相关内容