探索一种测试 Quarkus 中 CDI 组件的新方法
Quarkus 组件模型建立在 CDI 之上。然而,在没有运行的 CDI 容器的情况下为 bean 编写单元测试通常是一项繁琐的工作。如果没有容器服务启动并运行,所有工作都必须手动完成。首先,不执行任何依赖注入。此外,不触发事件,也不通知观察者。此外,拦截器也不适用。总之,一切都需要手动连接。但 Quarkus 可以做得更好,对吧?当然可以!Quarkus 3.2 引入了一项实验性功能,用于简化 CDI 组件的测试及其依赖项的模拟。这是一个轻量级的 JUnit 5 扩展,它不启动完整的 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.Foo
。Charlie
依赖项是一个自动创建的模拟。bar
配置属性的值使用 @TestConfigProperty
注释设置。
它是如何工作的?
QuarkusComponentTestExtension
在 before all
测试阶段执行几项操作。它启动 ArC - Quarkus 中的 CDI 容器。它还注册了一个专用的配置对象。然后在 after all
测试阶段停止容器并释放配置。用 @Inject
和 @InjectMock
注释的字段在创建测试实例后被注入。最后,CDI 请求上下文在每个测试方法中被激活和终止。
被测试的组件
默认情况下,所有用 @Inject
注释的字段的类型都被视为组件类型。但是,您还可以指定其他测试组件:使用 @QuarkusComponentTest#value()
或以编程方式作为 QuarkusComponentTestExtension(Class<?>…)
构造函数的参数。最后,在测试类上声明的静态嵌套类也是组件。
在带有 @QuarkusComponentTest 注释的测试类上声明的静态嵌套类在运行常规 @QuarkusTest 时会从 bean 发现中排除,以防止意外的 CDI 冲突。 |
高级功能
可以以编程方式配置 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 注释类型为 QuarkusComponentTestExtension 的 static 字段,并以编程方式配置扩展。 |
有时您需要完全控制 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 | 拦截器类会自动被视为被测试的组件,因此在测试执行期间使用。 |