测试

测试基础设施

师成师成· 更新于 2026-09-28· 阅读 21 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

测试基础设施

Camel 测试基础设施中的组件提供了一系列实用工具,用于简化与 Camel 以及可能与之交互的其他系统的测试。

测试基础设施分为两部分:

  • 一部分为所有作用域提供容器供应功能
  • 另一部分为测试提供容器供应功能

模拟测试基础设施

实施新测试时,首要步骤之一就是确定如何模拟其运行所需的基础设施。test-infra 模块提供了一个可复用的基础设施库,可用于此目的。

一般而言,集成测试利用项目 TestContainers 提供的功能,并使用容器镜像来模拟环境。此外,它还支持针对远程环境运行测试,以及在可用时使用可嵌入组件。具体细节因组件而异,建议查看代码以获取更多信息。

编写新的测试基础设施模块

本节面向需要编写新测试基础设施组件的 Camel 维护者。最终用户可跳过本节。

测试代码将测试环境的供应过程抽象为服务类(例如 JMSService、JDBCService 等)。服务类的目的是同时抽象服务类型(例如 Kafka、Strimzi 等)和服务位置(例如远程、本地、嵌入式等)。这提供了灵活性,可在不同情况下测试代码(例如使用远程 JMS 代理,或使用由 TestContainers 管理的容器中运行的本地 JMS 代理)。它使触发边缘情况以及尝试不同的运行场景(例如更高延迟、缓慢的后端等)变得更加容易。

当 TestContainers 中没有可用的容器镜像时,测试可以使用官方提供的镜像自行实现。许可证必须与 Apache 2.0 兼容。如果官方镜像不可用,则可以提供一个 Dockerfile 来构建该服务。Dockerfile 应尽可能减小容器的体积和资源占用。

容器信息必须存放在名为 container.properties 的文件中,该文件应包含容器的完全限定名称:

opensearch.container=mirror.gcr.io/opensearchproject/opensearch:2.18.0
opensearch.container.ppc64le=icr.io/ppc64le-oss/opensearch-ppc64le:2.12.0

键必须遵循 <name>.properties 模式。可以在键中加入具体的架构标识,以指明每种架构所使用的容器。目前接受的取值有:

  • aarch64:用于 Arm 架构
  • s390x:用于 s390x(Linux On Mainframe)
  • ppc64le:用于 64 位小端序 Power 架构

必要时也可以使用可嵌入组件,但这通常会导致代码更多、维护成本更高。

对可嵌入组件的支持可能会在未来的版本中被移除。

测试基础设施模块的推荐结构

所有类(服务接口、容器实现、JUnit 扩展和服务工厂)都放在 src/main 下。这样,这些模块就可以作为常规的 JAR 依赖被使用,而无需 test-jar 打包方式。

主源码

服务应提供一个以所实现的基础设施命名的接口,并且该接口应继承 InfrastructureService 接口。

理想情况下,服务应有两个具体实现:一个是远程服务的实现(如适用),另一个是容器服务的实现:

              MyService
                 / \
                /   \
               /     \
 MyRemoteService    MyContainerService

在大多数情况下,由专门的服务工厂类负责根据运行时参数和其他测试场景的约束来创建服务。当一个服务允许选择不同的服务类型或位置时,应通过命令行属性(-D<property.name>=<value>)来指定。例如,当允许服务在本地容器和远程实例之间进行选择时,应处理格式为 <name>.instance.type 的属性。不同场景中使用的其他运行时参数应处理为 <name>.<parameter>。更复杂的服务可以通过工厂类提供的 builder 来相应地组合服务。

TestService 接口可用于将实际的 Service 实现与 JUnit 及其生命周期集成。服务在运行时应尽量减少测试执行时间和资源消耗。因此,JUnit 的 @BeforeAllCallback 和 @AfterAllCallback 应尽可能作为首选扩展,因为它们允许基础设施实例在整个测试执行过程中保持静态。

请注意,根据 JUnit 6 扩展模型,服务初始化的时间可能会因服务实例在测试类中是否声明为静态而有所不同。因此,代码不应对其初始化时间做任何假设。

注册属性

所有服务都应通过 System.setProperty 注册允许访问服务的属性。这是在使用 Spring 框架运行测试时解析这些属性所必需的。此注册允许在 Spring 的 XML 文件中解析这些属性。

该注册在服务初始化期间的 registerProperties 方法中完成。

注册属性示例:

在具体的服务实现中注册属性:

仅 Java:为测试基础设施注册服务属性

    public void registerProperties() {
        // MyServiceProperties.MY_SERVICE_HOST is a string with value "my.service.host"
        System.setProperty(MyServiceProperties.MY_SERVICE_HOST, container.getHost());

        // MyServiceProperties.MY_SERVICE_PORT is a string with value "my.service.port"
        System.setProperty(MyServiceProperties.MY_SERVICE_PORT, String.valueOf(container.getServicePort()));

        // MyServiceProperties.MY_SERVICE_ADDRESS is a string with value "my.service.address"
        System.setProperty(MyServiceProperties.MY_SERVICE_ADDRESS, getServiceAddress());
    }

    public void initialize() {
        LOG.info("Trying to start the MyService container");
        container.start();

        registerProperties();
        LOG.info("MyService instance running at {}", getServiceAddress());
    }

然后,在 Camel 路由或 Spring XML 属性中引用这些属性时,可以使用 {{my.service.host}}、{{my.service.port}} 和 {{my.service.address}}。

打包建议

测试基础设施模块以常规 JAR 构件的形式打包。父级 pom 应当提供打包测试基础设施所需的一切内容。

在测试中使用测试基础设施

在新的组件测试中使用测试基础设施相当简单,与使用其他任何可复用组件类似。首先在 pom 文件中声明测试基础设施的依赖项。

内容大致如下:

<!-- test infra -->
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-test-infra-myservice</artifactId>
    <version>${project.version}</version>
    <scope>test</scope>
</dependency>
在上述依赖中,依赖版本被设置为 ${project.version}。当在 Camel Core 项目之外使用时,应将其调整为所使用的 Camel 版本。

在测试类中,为该服务添加一个成员变量,并使用 @RegisterExtension 对其进行注解,以便由 JUnit 管理其生命周期。

仅 Java:将测试基础设施服务注册为 JUnit 扩展

@RegisterExtension
static MyService service = MyServiceServiceFactory.createSingletonService();

单例服务

大多数服务工厂都提供一个 createSingletonService() 方法,返回一个 JVM 范围内的单例实例。这是集成测试的推荐方式。该单例可确保 Testcontainers 服务(或内嵌服务)在每个 JVM 中只启动一次,并在所有测试类之间复用,而不是为每个测试类都启动一个新容器。

单例服务的优势:

  • 测试执行更快 —— 容器只启动一次并被复用,避免了重复的启动开销。
  • 支持并行测试 —— 在并行运行测试时(例如使用 mvnd),单例可防止多个测试类在相同端口上启动相互冲突的容器。
  • 资源占用更低 —— 单个容器实例服务于所有测试类,而不是每个类各自管理自己的容器。

该单例基于静态内部类延迟加载的惯用写法实现,并包含一个 JVM 关闭钩子,在 JVM 退出时停止容器。JUnit 的 @RegisterExtension 生命周期对单例调用 shutdown() 会被视为无操作,因此该服务会跨越测试类持续存活。

使用单例服务时,测试必须注意共享状态。例如,为每个测试类使用唯一的主题或队列名称,或在测试之间清理数据,以避免相互干扰。

只有当测试确实需要自己独立的服务实例时(例如,它以会破坏其他测试的方式重新配置容器),才应使用 createService()。

更复杂的测试服务可以使用类似如下的方式创建:

仅 Java:使用构建器模式创建复杂的测试服务

@RegisterExtension
static MyService service = MyServiceServiceFactory
    .builder()
        .addRemoveMapping(MyTestClass::myCustomRemoteService) // this is rarely necessary
        .addLocalMapping(MyTestClass::staticMethodReturningAService) // sets the handler for -Dmy-service.instance.type=local-myservice-local-container
        .addMapping("local-alternative-service", MyTestClass::anotherMethodReturningAService) // sets the handler for -Dmy-service.instance.type=local-alternative-service
    .createService();

你可以使用这些方法以及已注册的属性来访问可用的测试基础设施服务。在 Spring XML 文件中使用这些属性时,你也可以使用这些属性。

<someSpringXmlElement httpHost="{{my.service.host}}" port="{{my.service.port}}" />

也可以在测试代码本身中使用这些属性。例如,在为 Camel 组件设置测试 URL 时:

仅限 Java:在 Camel 路由中使用测试基础设施属性

    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            public void configure() {
                from("direct:put")
                    .to("mycomponent:someoption?host={{my.service.host}}&port={{my.service.port}}");
            }
        };
    }

执行顺序

在组合使用测试基础设施的不同模块时,你可能需要确保它们按照正确的顺序执行。这可以通过使用 JUnit 的 @Order 注解来实现。

例如:

仅 Java:控制测试扩展的执行顺序

    @Order(1)
    @RegisterExtension
    protected static KafkaService service = KafkaServiceFactory.createSingletonService();

    @Order(2)
    @RegisterExtension
    protected static CamelContextExtension contextExtension = new DefaultCamelContextExtension();

容器运行时支持

本模块中的大部分测试基础设施都基于容器。因此,运行这些测试需要一个容器运行时。测试是使用以下运行时编写并验证的:

Podman 支持

假设 Podman 已正确安装并配置为与 docker 行为一致(例如:短名称解析、解析 docker.io 镜像仓库等),使用 Podman 的唯一要求是在运行测试之前导出 DOCKER_HOST 变量。

Linux

在大多数系统上,这应该类似于以下命令:

export DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock

OS X 和 Windows

在 OS X 和 Windows 上使用 Podman 运行 test-infra 在许多情况下应当可行。但是,它需要额外的步骤,并且存在一些问题。因此,目前不建议这样做。

已知问题和/或提示

多架构支持

某些容器没有针对所有架构的可用镜像。在这种情况下,建议:

  1. 如果信誉良好的来源提供了适用于该架构的镜像,则使用其替代镜像。
  2. 如果该系统在该架构上可用,则创建 Dockerfile 并自行构建。
  3. 在该架构上禁用相关测试。

评论

登录后参与评论

正在加载评论…