编辑此页面

测试组件

Quarkus 的组件模型构建于 CDI 之上。因此,Quarkus 提供了 QuarkusComponentTestExtension - 一个 JUnit 扩展,可以轻松测试组件/CDI Bean 并模拟它们的依赖项。与 @QuarkusTest 不同,此扩展不会启动完整的 Quarkus 应用程序,而只会启动 CDI 容器和配置服务。您可以在 生命周期 部分找到更多详细信息。

此扩展在 quarkus-junit5-component 依赖项中可用。

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 内置参数,例如 RepetitionInfoTestInfo,会被自动跳过。

@Inject 注入点接收 CDI Bean 的上下文实例 - 正在测试的真实组件。@InjectMock 注入点接收为 自动模拟的未满足依赖项 创建的“未配置的”Mockito Mock。

注入到字段和测试方法参数中的 Dependent Bean 在测试实例销毁之前和测试方法完成后分别被正确销毁。

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

3.1. 测试的组件

测试的组件的初始集合是从测试类派生的

  1. 所有用 @jakarta.inject.Inject 注释的字段的类型都被认为是组件类型。

  2. 未用 @InjectMock@SkipInject@org.mockito.Mock 注释的测试方法参数的类型也被认为是组件类型。

  3. 如果 @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 配置行为。

@InjectMock 不打算作为 Mockito JUnit 扩展提供的功能的通用替代品。它旨在用于配置 CDI Bean 的未满足依赖项。您可以并排使用 QuarkusComponentTestMockitoExtension

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());
    }
}

3.3. 未满足依赖项的自定义 Mock

有时您需要完全控制 Bean 属性,甚至可能配置默认的 Mock 行为。您可以通过 QuarkusComponentTestExtensionBuilder#mock() 方法使用 Mock 配置器 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 定义拦截类型。

相关内容