Testing components

Quarkus 的组件模型建立在 CDI之上。因此,Quarkus 提供 QuarkusComponentTestExtension - 一个 JUnit 扩展,它能轻松地测试组件/CDI Bean 并模拟它们的依赖性。与 `@QuarkusTest`不同,此扩展不会启动完整的 Quarkus 应用程序,而仅仅是 CDI 容器和配置服务。在 Lifecycle部分中可以找到更多详细信息。

The component model of Quarkus is built on top CDI. Therefore, Quarkus provides QuarkusComponentTestExtension - a JUnit extension that makes it easy to test the components/CDI beans and mock their dependencies. Unlike @QuarkusTest this extension does not start a full Quarkus application but merely the CDI container and the configuration service. You can find more details in the Lifecycle section.

此扩展在 `quarkus-junit5-component`依赖性中提供。

This extension is available in the quarkus-junit5-component dependency.

Basic example

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

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

Foo component
package org.acme;

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

@ApplicationScoped 1
public class Foo {

    @Inject
    Charlie charlie; 2

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

    public String ping() {
        return bar ? charlie.ping() : "nok";
    }
}
1 Foo is an @ApplicationScoped CDI bean.
2 Foo depends on Charlie which declares a method ping().
3 Foo depends on the config property bar. @Inject is not needed for this injection point because it also declares a CDI qualifier - this is a Quarkus-specific feature.

然后,一个组件测试看起来像这样:

Then a component test could look like:

Simple component test
import static org.junit.jupiter.api.Assertions.assertEquals;

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

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

    @Inject
    Foo foo; 3

    @InjectMock
    Charlie charlieMock; 4

    @Test
    public void testPing() {
        Mockito.when(charlieMock.ping()).thenReturn("OK"); 5
        assertEquals("OK", foo.ping());
    }
}
1 The QuarkusComponentTest annotation registers the JUnit extension.
2 Sets a configuration property for the test.
3 The test injects the component under the test. The types of all fields annotated with @Inject are considered the component types under test. You can also specify additional component classes via @QuarkusComponentTest#value(). Furthermore, the static nested classes declared on the test class are components too.
4 The test also injects a mock for Charlie. Charlie is an unsatisifed dependency for which a synthetic @Singleton bean is registered automatically. The injected reference is an "unconfigured" Mockito mock.
5 We can leverage the Mockito API in a test method to configure the behavior.

`QuarkusComponentTestExtension`还解决了测试方法的参数,并注入匹配的 Bean。

QuarkusComponentTestExtension also resolves parameters of test methods and injects matching beans.

所以以上代码段可以重新编写为:

So the code snippet above can be rewritten as:

Simple component test with test method parameters
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.

此外,如果你需要完全控制 `QuarkusComponentTestExtension`配置,则可以使用 `@RegisterExtension`注释,并通过编程方式配置扩展。

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:

Simple component test with programmatic configuration
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.

Lifecycle

那么 `QuarkusComponentTest`到底做了些什么呢?它启动 CDI 容器,并注册一个专用的 configuration object

So what exactly does the QuarkusComponentTest do? It starts the CDI container and registers a dedicated configuration object.

如果测试实例生命周期是 Lifecycle#PER_METHOD(默认),则在 before each`测试阶段启动容器,并在 `after each`测试阶段停止容器。但是,如果测试实例生命周期是 `Lifecycle#PER_CLASS,则在 `before all`测试阶段启动容器,并在 `after all`测试阶段停止容器。

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.

在创建测试实例之后,使用 `@Inject`和 `@InjectMock`注释的字段就会注入。测试方法的参数(它有一个匹配的 Bean)是在执行测试方法时解析的(除非使用 `@io.quarkus.test.component.SkipInject`或 `@org.mockito.Mock`注释)。最后,每个测试方法都会激活并终止 CDI 请求上下文。

The fields annotated with @Inject and @InjectMock are injected after a test instance is created. The parameters of a test method for which a matching bean exists are resolved (unless annotated with @io.quarkus.test.component.SkipInject or @org.mockito.Mock) when a test method is executed. Finally, the CDI request context is activated and terminated per each test method.

Injection

使用 @jakarta.inject.Inject`和 `@io.quarkus.test.InjectMock`注解的测试类字段会在测试实例创建后注入。此外,如果存在匹配的 bean,将解析测试方法的参数,除非使用 `@io.quarkus.test.component.SkipInject`或 `@org.mockito.Mock`注解。还有一些 JUnit 内置参数,例如 `RepetitionInfo`和 `TestInfo,会被自动跳过。

Fields of the test class that are annotated with @jakarta.inject.Inject and @io.quarkus.test.InjectMock are injected after a test instance is created. Furthermore, the parameters of a test method for which a matching bean exists are resolved unless annotated with @io.quarkus.test.component.SkipInject or @org.mockito.Mock. There are also some JUnit built-in parameters, such as RepetitionInfo and TestInfo, which are skipped automatically.

@Inject`注入点会接收 CDI bean 的上下文实例,即待测的真实组件。@InjectMock`注入点会接收为 unsatisfied dependency automatically创建的“未配置”的 Mockito 桩件。

An @Inject injection point receives the contextual instance of a CDI bean - the real component under test. An @InjectMock injection point receives an "unconfigured" Mockito mock that was created for an auto_mocking.

注入到字段和测试方法参数中的依赖 bean 会在测试实例被销毁之前,以及测试方法完成后分别被正确销毁。

Dependent beans injected into the fields and test method arguments are correctly destroyed before a test instance is destroyed and after the test method completes, respectively.

ArgumentsProvider`提供的 `@ParameterizedTest`方法的参数,例如带有 `@org.junit.jupiter.params.provider.ValueArgumentsProvider,必须使用 `@SkipInject`注解。

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.

Auto Mocking Unsatisfied Dependencies

与常规 CDI 环境不同,如果组件注入了一个未满足的依赖项,测试不会失败。相反,系统会为注入点中必需类型和限定符的每一个组合自动注册一个合成的 bean,该注入点会解析为一个未满足的依赖项。该 bean 具有 `@Singleton`作用域,因此在具有相同必需类型和限定符的所有注入点之间共享。注入的引用是 _unconfigured_Mockito 桩件。你可以使用 `io.quarkus.test.InjectMock`注解在测试中注入该桩件,并利用 Mockito API 配置其行为。

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.

`@InjectMock`并非打算作为 Mockito JUnit 扩展提供之功能的通用替代品。它用于配置 CDI bean 的未满足依赖项。你可以将 `QuarkusComponentTest`和 `MockitoExtension`搭配使用。

@InjectMock is not intended as a universal replacement for functionality provided by the Mockito JUnit extension. It’s meant to be used for configuration of unsatisfied dependencies of CDI beans. You can use the QuarkusComponentTest and MockitoExtension side by side.

import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
@QuarkusComponentTest
public class FooTest {

    @TestConfigProperty(key = "bar", value = "true")
    @Test
    public void testPing(Foo foo, @InjectMock Charlie charlieMock, @Mock Ping ping) {
        Mockito.when(ping.pong()).thenReturn("OK");
        Mockito.when(charlieMock.ping()).thenReturn(ping);
        assertEquals("OK", foo.ping());
    }
}

Custom Mocks For Unsatisfied Dependencies

有时,你需要对 bean 属性有完全的控制权,甚至可以配置默认的桩件行为。你可以通过 `QuarkusComponentTestExtensionBuilder#mock()`方法使用桩件配置器 API。

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.

Configuration

可以使用 @io.quarkus.test.component.TestConfigProperty`注解或 `QuarkusComponentTestExtensionBuilder#configProperty(String, String)`方法为测试设置配置属性。如果你只需要使用缺失配置属性的默认值,则可以使用 `@QuarkusComponentTest#useDefaultConfigProperties()`或 `QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()

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.

也可以使用 @io.quarkus.test.component.TestConfigProperty`注解为测试方法设置配置属性。但是,如果测试实例生命周期为 `Lifecycle#_PER_CLASS,则此注解只能在测试类中使用,而在测试方法中会被忽略。

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.

还为所有注入的 Config Mappings自动注册 CDI bean。这些映射会使用测试配置属性填充。

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

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.

在使用 `@QuarkusComponentTest`注解的测试类中声明的静态嵌套类会在运行 `@QuarkusTest`时从 bean 发现中排除,以防止无意的 CDI 冲突。

Static nested classes 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.

第二种选择是在测试类中直接声明一个拦截器方法;然后会在相关的拦截阶段调用该方法。

The second option is to declare an interceptor method directly in the test class; the 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.