测试组件
1. 基本示例
让我们有一个组件 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 来配置行为。 |
QuarkusComponentTestExtension
还会解析测试方法的参数并注入匹配的 Bean。
因此,上面的代码片段可以重写为
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
注释并以编程方式配置扩展。
原始测试可以重写为
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 在测试类的静态字段中配置。 |
2. 生命周期
那么 QuarkusComponentTest
到底做了什么?它启动 CDI 容器并注册一个专用的 配置对象。
如果测试实例生命周期是 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 请求上下文被激活并在每个测试方法中终止。
3. 注入
用 @jakarta.inject.Inject
和 @io.quarkus.test.InjectMock
注释的测试类的字段在创建测试实例后注入。此外,测试方法的参数,只要存在匹配的 Bean,就会被解析,除非用 @io.quarkus.test.component.SkipInject
或 @org.mockito.Mock
注释。还有一些 JUnit 内置参数,例如 RepetitionInfo
和 TestInfo
,会被自动跳过。
@Inject
注入点接收 CDI Bean 的上下文实例 - 正在测试的真实组件。@InjectMock
注入点接收为 自动模拟的未满足依赖项 创建的“未配置的”Mockito Mock。
注入到字段和测试方法参数中的 Dependent Bean 在测试实例销毁之前和测试方法完成后分别被正确销毁。
由 ArgumentsProvider 提供的 @ParameterizedTest 方法的参数,例如使用 @org.junit.jupiter.params.provider.ValueArgumentsProvider ,必须用 @SkipInject 注释。 |
3.1. 测试的组件
测试的组件的初始集合是从测试类派生的
-
所有用
@jakarta.inject.Inject
注释的字段的类型都被认为是组件类型。 -
未用
@InjectMock
、@SkipInject
或@org.mockito.Mock
注释的测试方法参数的类型也被认为是组件类型。 -
如果
@QuarkusComponentTest#addNestedClassesAsComponents()
设置为true
(默认),则在测试类上声明的所有静态嵌套类也被认为是组件。
@Inject Instance<T> 和 @Inject @All List<T> 注入点被特别处理。实际的类型参数被注册为组件。但是,如果类型参数是一个接口,则实现不会被自动注册。 |
可以使用 @QuarkusComponentTest#value()
或 QuarkusComponentTestExtensionBuilder#addComponentClasses()
设置其他组件类。
3.2. 自动模拟未满足的依赖项
与常规 CDI 环境不同,如果组件注入未满足的依赖项,测试不会失败。相反,对于解析为未满足的依赖项的注入点的所需类型和限定符的每个组合,会自动注册一个合成 Bean。Bean 具有 @Singleton
作用域,因此它在所有具有相同所需类型和限定符的注入点之间共享。注入的引用是一个未配置的 Mockito Mock。您可以使用 io.quarkus.test.InjectMock
注释在测试中注入 Mock,并利用 Mockito API 配置行为。
|
4. 嵌套测试
JUnit 5 @Nested tests 可能有助于构建更复杂的测试场景。但是,只有基本用例通过 @QuarkusComponentTest
进行测试。
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)
@Nested
class PingTest {
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
@Nested
class PongTest {
@Test
public void testPong() {
Mockito.when(charlieMock.pong()).thenReturn("NOK");
assertEquals("NOK", foo.pong());
}
}
}
1 | QuarkusComponentTest 注释注册 JUnit 扩展。 |
2 | 为测试设置配置属性。 |
3 | 测试注入正在测试的组件。Foo 注入 Charlie 。 |
4 | 该测试还注入了 Charlie 的 Mock。注入的引用是一个“未配置的”Mockito Mock。 |
5. 配置
您可以使用 @io.quarkus.test.component.TestConfigProperty
注释或 QuarkusComponentTestExtensionBuilder#configProperty(String, String)
方法为测试设置配置属性。如果您只需要使用缺失配置属性的默认值,那么 @QuarkusComponentTest#useDefaultConfigProperties()
或 QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()
可能会派上用场。
也可以使用 @io.quarkus.test.component.TestConfigProperty
注释为测试方法设置配置属性。但是,如果测试实例生命周期是 Lifecycle#_PER_CLASS
,则此注释只能在测试类上使用,而在测试方法上会被忽略。
在 @Nested 测试类上声明的 @io.quarkus.test.component.TestConfigProperty 始终被忽略。 |
CDI Bean 也会自动为所有注入的 Config Mappings 注册。这些映射用测试配置属性填充。
6. 模拟 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());
}
@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 是一个拦截器绑定。 |
2 | 拦截器类会自动被视为测试的组件。 |
在测试类上声明的用 @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 | 生成的拦截器的拦截器绑定通过用拦截器绑定类型注释该方法来指定。 |
2 | 定义拦截类型。 |