Testing components

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

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

Basic example

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

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

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

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>  `QuarkusComponentTest`注解注册 JUnit 扩展。
<1>  为测试设置配置属性。
<1>  测试注入待测组件。所有使用 `@Inject`进行注解的字段类型都被认为是待测组件类型。你还可以通过 `@QuarkusComponentTest#value()`指定附加组件类。此外,在测试类中声明的静态嵌套类也是组件。
<1>  该测试还为 `Charlie`注入一个模拟对象。`Charlie`是 _unsatisifed_依赖项,系统会自动为其注册一个合成的 `@Singleton`Bean。注入的引用是一个“未配置”的 Mockito 模拟对象。
<1>  我们可以在一个测试方法中利用 Mockito API 来配置行为。

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

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

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>  用 `@io.quarkus.test.component.SkipInject`注释的参数绝不会通过此扩展解析。

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

原始测试可以这样重新编写:

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>  `QuarkusComponentTestExtension`在测试类的静态字段中配置。

Lifecycle

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

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

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

Injection

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

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

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

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

Auto Mocking Unsatisfied Dependencies

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

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

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。

Configuration

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

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

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

Mocking CDI Interceptors

如果已测试的组件类声明了一个拦截器绑定,你可能也需要对拦截进行桩件处理。有两种方法可以执行此任务。首先,你可以将拦截器类定义为测试类的静态嵌套类。

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`是一个拦截器绑定。
<1>  拦截器类会自动被视为测试组件。

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

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

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>  结果拦截器的拦截器绑定通过使用拦截器绑定类型对该方法进行注解来指定。
<1>  Defines the interception type.