探索一种测试 Quarkus 中 CDI 组件的新方法

Quarkus 组件模型建立在 CDI 之上。然而,在没有运行的 CDI 容器的情况下为 bean 编写单元测试通常是一项繁琐的工作。如果没有容器服务启动并运行,所有工作都必须手动完成。首先,不执行任何依赖注入。此外,不触发事件,也不通知观察者。此外,拦截器也不适用。总之,一切都需要手动连接。但 Quarkus 可以做得更好,对吧?当然可以!Quarkus 3.2 引入了一项实验性功能,用于简化 CDI 组件的测试及其依赖项的模拟。这是一个轻量级的 JUnit 5 扩展,它不启动完整的 Quarkus 应用程序,而是仅运行使测试成为愉悦体验所需的服务。

一个简单的例子

首先,将 quarkus-junit5-component 模块依赖添加到您的项目中。

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

现在,想象一下我们有一个组件 Foo,它是一个 @ApplicationScoped CDI bean。

package org.acme;

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

@ApplicationScoped
public class Foo {

    @Inject
    Charlie charlie; (1)

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

    public String ping() { (3)
        return bar ? charlie.ping() : "nok";
    }
}
1 Foo 依赖于 Charlie,后者声明了一个方法 ping()
2 Foo 依赖于配置属性 bar
3 目标是测试此方法,该方法使用了 Charlie 依赖项和 bar 配置属性。

然后,一个简单的组件测试如下所示:

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 注册了 QuarkusComponentTestExtension,它在后台完成了所有繁重的工作。
2 @TestConfigProperty 用于为测试设置配置属性的值。
3 测试注入了被测试的组件。所有用 @Inject 注释的字段的类型都被视为被测试的组件类型。
4 测试还注入了一个 Charlie 的模拟。Charlie 是一个依赖项,它会自动注册一个 @Singleton bean。注入的引用是一个“未配置”的 Mockito 模拟。
5 Mockito API 用于配置注入的模拟的行为。

在此特定测试中,唯一“真正”被测试的组件是 org.acme.FooCharlie 依赖项是一个自动创建的模拟。bar 配置属性的值使用 @TestConfigProperty 注释设置。

它是如何工作的?

QuarkusComponentTestExtensionbefore all 测试阶段执行几项操作。它启动 ArC - Quarkus 中的 CDI 容器。它还注册了一个专用的配置对象。然后在 after all 测试阶段停止容器并释放配置。用 @Inject@InjectMock 注释的字段在创建测试实例后被注入。最后,CDI 请求上下文在每个测试方法中被激活和终止。

被测试的组件

默认情况下,所有用 @Inject 注释的字段的类型都被视为组件类型。但是,您还可以指定其他测试组件:使用 @QuarkusComponentTest#value() 或以编程方式作为 QuarkusComponentTestExtension(Class<?>…​) 构造函数的参数。最后,在测试类上声明的静态嵌套类也是组件。

在带有 @QuarkusComponentTest 注释的测试类上声明的静态嵌套类在运行常规 @QuarkusTest 时会从 bean 发现中排除,以防止意外的 CDI 冲突。

自动模拟未满足的依赖项

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

配置

before all 测试阶段注册了一个专用的 SmallRyeConfig。可以使用 @TestConfigProperty 注释或以编程方式使用 QuarkusComponentTestExtension#configProperty(String, String) 方法来设置配置属性。如果您需要为缺失的配置属性使用默认值,那么 @QuarkusComponentTest#useDefaultConfigProperties()QuarkusComponentTestExtension#useDefaultConfigProperties() 可能会很有用。

高级功能

可以以编程方式配置 QuarkusComponentTestExtension。上面的简单示例可以重写为:

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 = new QuarkusComponentTestExtension()
            .configProperty("bar","true");

    @Inject
    Foo foo;

    @InjectMock
    Charlie charlieMock;

    @Test
    public void testPing() {
        Mockito.when(charlieMock.ping()).thenReturn("OK");
        assertEquals("OK", foo.ping());
    }
}
1 @RegisterExtension 注释类型为 QuarkusComponentTestExtensionstatic 字段,并以编程方式配置扩展。

有时您需要完全控制 bean 属性,甚至可能需要配置模拟依赖项的默认行为。在这种情况下,模拟配置器 API 和 QuarkusComponentTestExtension#mock() 方法是正确的选择。

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

import jakarta.enterprise.context.Dependent;
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
    static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension()
            .configProperty("bar","true")
            .mock(Charlie.class)
                .scope(Dependent.class) (1)
                .createMockitoMock(mock -> {
                    Mockito.when(mock.pong()).thenReturn("BAR"); (2)
                });

    @Inject
    Foo foo;

    @Test
    public void testPing() {
        assertEquals("BAR", foo.ping());
    }
}
1 被模拟 bean 的范围是 @Dependent
2 配置模拟的默认行为。

模拟 CDI 拦截器

此功能仅在 Quarkus 3.3+ 中可用。

如果被测试的组件类声明了拦截器绑定,那么您可能还需要模拟拦截。您可以将模拟拦截器类定义为测试类的静态嵌套类。然后会自动发现该拦截器类。

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 SimpleMockInterceptor {  (2)

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

    }
}
1 @SimpleBinding 是一个拦截器绑定。
2 拦截器类会自动被视为被测试的组件,因此在测试执行期间使用。

总结

在本文中,我们讨论了在 Quarkus 应用程序中测试 CDI 组件的新方法的可能性。