测试支持

Spring Integration 提供了许多实用工具和注解来帮助您测试应用程序。测试支持由两个模块提供

  • spring-integration-test-support 包含核心项目和共享实用工具

  • spring-integration-test 为集成测试提供模拟和应用程序上下文配置组件

spring-integration-test-support(5.0 之前的版本为 spring-integration-test)提供用于单元测试的基本、独立的实用工具、规则和匹配器。(它本身不依赖于 Spring Integration,并在框架内部测试中使用)。spring-integration-test 旨在帮助进行集成测试,并提供全面的高级 API 来模拟集成组件并验证单个组件(包括整个集成流或仅其部分)的行为。

本参考手册的范围不涉及企业中的全面测试。有关测试目标集成解决方案的思路和原则,请参阅 Gregor Hohpe 和 Wendy Istvanick 的论文 “企业集成项目中的测试驱动开发”

Spring Integration 测试框架和测试实用工具完全基于现有的 JUnit、Hamcrest 和 Mockito 库。应用程序上下文交互基于 Spring 测试框架。有关这些项目的更多信息,请参阅其文档。

得益于 Spring Integration 框架中 EIP 的规范实现及其一流的“公民”(例如 MessageChannelEndpointMessageHandler)、抽象和松耦合原则,您可以实现任何复杂性的集成解决方案。借助用于流定义的 Spring Integration API,您可以改进、修改甚至替换流的某些部分,而不会(大部分)影响集成解决方案中的其他组件。测试此类集成解决方案仍然是一个挑战,无论是从端到端方法还是从隔离方法来看。一些现有工具可以帮助测试或模拟某些集成协议,并且它们与 Spring Integration 通道适配器配合良好。此类工具的示例包括

  • Spring MockMVC 及其 MockRestServiceServer 可用于测试 HTTP。

  • 一些 RDBMS 供应商提供嵌入式数据库以支持 JDBC 或 JPA。

  • ActiveMQ 可以嵌入用于测试 JMS 或 STOMP 协议。

  • 有用于嵌入式 MongoDB 和 Redis 的工具。

  • Tomcat 和 Jetty 具有嵌入式库,用于测试真实的 HTTP、Web 服务或 WebSockets。

  • Apache Mina 项目的 FtpServerSshServer 可用于测试 FTP 和 SFTP 协议。

  • Hazelcast 可以在测试中作为真实数据网格节点运行。

  • Curator 框架为 Zookeeper 交互提供了 TestingServer

  • Apache Kafka 提供了管理工具,可在测试中嵌入 Kafka Broker。

  • GreenMail 是一个开源、直观且易于使用的电子邮件服务器测试套件,用于测试目的。

这些工具和库中的大部分都用于 Spring Integration 测试。此外,您可以从 GitHub 存储库(在每个模块的 test 目录中)中发现如何构建自己的集成解决方案测试的思路。

本章的其余部分描述了 Spring Integration 提供的测试工具和实用工具。

测试实用工具

spring-integration-test-support 模块提供了用于单元测试的实用工具和帮助程序。

TestUtils

TestUtils 类主要用于 JUnit 测试中的属性断言,如以下示例所示

@Test
public void loadBalancerRef() {
    MessageChannel channel = channels.get("lbRefChannel");
    LoadBalancingStrategy lbStrategy = TestUtils.getPropertyValue(channel,
                 "dispatcher.loadBalancingStrategy", LoadBalancingStrategy.class);
    assertTrue(lbStrategy instanceof SampleLoadBalancingStrategy);
}

TestUtils.getPropertyValue() 基于 Spring 的 DirectFieldAccessor,并提供了从目标私有属性获取值的能力。如上例所示,它还支持使用点号表示法访问嵌套属性。

createTestApplicationContext() 工厂方法使用提供的 Spring Integration 环境生成 TestApplicationContext 实例。

有关此类的更多信息,请参阅其他 TestUtils 方法的 Javadoc

使用 OnlyOnceTrigger

OnlyOnceTrigger 对于轮询端点很有用,当您只需要生成一条测试消息并验证其行为而不会影响其他周期性消息时。以下示例展示了如何配置 OnlyOnceTrigger

<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" />

<int:poller id="jpaPoller" trigger="testTrigger">
    <int:transactional transaction-manager="transactionManager" />
</int:poller>

以下示例展示了如何使用上述 OnlyOnceTrigger 配置进行测试

@Autowired
@Qualifier("jpaPoller")
PollerMetadata poller;

@Autowired
OnlyOnceTrigger testTrigger;

@Test
@DirtiesContext
public void testWithEntityClass() throws Exception {
    this.testTrigger.reset();
    ...
    JpaPollingChannelAdapter jpaPollingChannelAdapter = new JpaPollingChannelAdapter(jpaExecutor);

    SourcePollingChannelAdapter adapter = JpaTestUtils.getSourcePollingChannelAdapter(
    		jpaPollingChannelAdapter, this.outputChannel, this.poller, this.context,
    		this.getClass().getClassLoader());
    adapter.start();
    ...
}

支持组件

org.springframework.integration.test.support 包包含您应该在目标测试中实现的各种抽象类

JUnit 条件

@LongRunningTest 条件注解用于指示是否应该在设置 RUN_LONG_INTEGRATION_TESTS 环境变量或系统属性为 true 时运行测试。否则,它将被跳过。

Hamcrest 和 Mockito 匹配器

org.springframework.integration.test.matcher 包包含几个 Matcher 实现,用于在单元测试中断言 Message 及其属性。以下示例展示了如何使用其中一个匹配器 (PayloadMatcher)

import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload;
...
@Test
public void transform_withFilePayload_convertedToByteArray() throws Exception {
    Message<?> result = this.transformer.transform(message);
    assertThat(result, is(notNullValue()));
    assertThat(result, hasPayload(is(instanceOf(byte[].class))));
    assertThat(result, hasPayload(SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING)));
}

MockitoMessageMatchers 工厂可用于模拟的存根和验证,如以下示例所示

static final Date SOME_PAYLOAD = new Date();

static final String SOME_HEADER_VALUE = "bar";

static final String SOME_HEADER_KEY = "test.foo";
...
Message<?> message = MessageBuilder.withPayload(SOME_PAYLOAD)
                .setHeader(SOME_HEADER_KEY, SOME_HEADER_VALUE)
                .build();
MessageHandler handler = mock(MessageHandler.class);
handler.handleMessage(message);
verify(handler).handleMessage(messageWithPayload(SOME_PAYLOAD));
verify(handler).handleMessage(messageWithPayload(is(instanceOf(Date.class))));
...
MessageChannel channel = mock(MessageChannel.class);
when(channel.send(messageWithHeaderEntry(SOME_HEADER_KEY, is(instanceOf(Short.class)))))
        .thenReturn(true);
assertThat(channel.send(message), is(false));

AssertJ 条件和谓词

从版本 5.2 开始,引入了 MessagePredicate 用于 AssertJ matches() 断言。它需要一个 Message 对象作为期望。此外,还可以配置要从期望中排除以及从实际消息中排除的头信息进行断言。

Spring Integration 和测试上下文

通常,Spring 应用程序的测试使用 Spring Test Framework。由于 Spring Integration 基于 Spring Framework 基础,因此我们使用 Spring Test Framework 所能做的一切也适用于测试集成流。org.springframework.integration.test.context 包提供了一些组件,用于增强集成需求的测试上下文。首先,我们使用 @SpringIntegrationTest 注解配置我们的测试类以启用 Spring Integration Test Framework,如以下示例所示

@SpringJUnitConfig
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {

    @Autowired
    private MockIntegrationContext mockIntegrationContext;

}

@SpringIntegrationTest 注解填充了一个 MockIntegrationContext bean,您可以将其自动装配到测试类以访问其方法。通过 noAutoStartup 选项,Spring Integration Test Framework 阻止通常 autoStartup=true 的端点启动。端点与提供的模式匹配,这些模式支持以下简单模式样式:xxx*xxx*xxxxxx*yyy

这在当我们需要避免与入站通道适配器(例如,AMQP 入站网关、JDBC 轮询通道适配器、客户端模式下的 WebSocket 消息生产者等)的目标系统建立实际连接时非常有用。

@SpringIntegrationTest 遵循 org.springframework.test.context.NestedTestConfiguration 语义,因此可以声明在外部类(甚至其超类)上——@SpringIntegrationTest 环境将可用于继承的 @Nested 测试。

MockIntegrationContext 旨在用于目标测试用例中,以修改实际应用程序上下文中的 bean。例如,可以将 autoStartup 被覆盖为 false 的端点替换为模拟,如以下示例所示

@Test
public void testMockMessageSource() {
    MessageSource<String> messageSource = () -> new GenericMessage<>("foo");

    this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", messageSource);

    Message<?> receive = this.results.receive(10_000);
    assertNotNull(receive);
}
这里的 mySourceEndpoint 指的是 SourcePollingChannelAdapter 的 bean 名称,我们用模拟替换了它的真实 MessageSource。类似地,MockIntegrationContext.substituteMessageHandlerFor() 期望 IntegrationConsumer 的 bean 名称,它将 MessageHandler 包装为端点。

执行测试后,您可以使用 MockIntegrationContext.resetBeans() 将端点 bean 的状态恢复到真实配置

@AfterEach
public void tearDown() {
    this.mockIntegrationContext.resetBeans();
}

从版本 6.3 开始,引入了 MockIntegrationContext.substituteTriggerFor() API。这可用于替换 AbstractPollingEndpoint 中的真实 Trigger。例如,生产配置可能依赖于每日(甚至每周)的 cron 计划。任何自定义 Trigger 都可以注入到目标端点以缓解时间跨度。例如,上面提到的 OnlyOnceTrigger 建议立即安排轮询任务并只执行一次的行为。

有关更多信息,请参阅 Javadoc

集成模拟

org.springframework.integration.test.mock 包提供了用于模拟、存根和验证 Spring Integration 组件上活动的工具和实用工具。模拟功能完全基于并兼容著名的 Mockito Framework。(当前的 Mockito 传递依赖项是 2.5.x 或更高版本。)

MockIntegration

MockIntegration 工厂提供了一个 API,用于为作为集成流一部分的 Spring Integration bean(MessageSourceMessageProducerMessageHandlerMessageChannel)构建模拟。您可以在配置阶段以及在目标测试方法中使用目标模拟来替换实际端点,然后再执行验证和断言,如以下示例所示

<int:inbound-channel-adapter id="inboundChannelAdapter" channel="results">
    <bean class="org.springframework.integration.test.mock.MockIntegration" factory-method="mockMessageSource">
        <constructor-arg value="a"/>
        <constructor-arg>
            <array>
                <value>b</value>
                <value>c</value>
            </array>
        </constructor-arg>
    </bean>
</int:inbound-channel-adapter>

以下示例展示了如何使用 Java 配置实现与上一个示例相同的配置

@InboundChannelAdapter(channel = "results")
@Bean
public MessageSource<Integer> testingMessageSource() {
    return MockIntegration.mockMessageSource(1, 2, 3);
}
...
StandardIntegrationFlow flow = IntegrationFlow
        .from(MockIntegration.mockMessageSource("foo", "bar", "baz"))
        .<String, String>transform(String::toUpperCase)
        .channel(out)
        .get();
IntegrationFlowRegistration registration = this.integrationFlowContext.registration(flow)
        .register();

为此,应在测试中使用上述 MockIntegrationContext,如以下示例所示

this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint",
        MockIntegration.mockMessageSource("foo", "bar", "baz"));
Message<?> receive = this.results.receive(10_000);
assertNotNull(receive);
assertEquals("FOO", receive.getPayload());

与 Mockito MessageSource 模拟对象不同,MockMessageHandler 是一个常规的 AbstractMessageProducingHandler 扩展,带有一个链式 API,用于对传入消息进行存根处理。MockMessageHandler 提供了 handleNext(Consumer<Message<?>>) 来为下一个请求消息指定一个单向存根。它用于模拟不产生回复的消息处理程序。handleNextAndReply(Function<Message<?>, ?>) 用于对下一个请求消息执行相同的存根逻辑并为其生成回复。它们可以链式调用以模拟所有预期请求消息变体的任意请求-回复场景。这些消费者和函数按顺序(从堆栈中)应用于传入消息,直到最后一个,然后将其用于所有剩余的消息。此行为类似于 Mockito AnswerdoReturn() API。

此外,您可以在构造函数参数中向 MockMessageHandler 提供一个 Mockito ArgumentCaptor<Message<?>>MockMessageHandler 的每个请求消息都由该 ArgumentCaptor 捕获。在测试期间,您可以使用其 getValue()getAllValues() 方法来验证和断言这些请求消息。

MockIntegrationContext 提供了一个 substituteMessageHandlerFor() API,允许您将实际配置的 MessageHandler 替换为被测端点中的 MockMessageHandler

以下示例展示了一个典型的使用场景

ArgumentCaptor<Message<?>> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);

MessageHandler mockMessageHandler =
        mockMessageHandler(messageArgumentCaptor)
                .handleNextAndReply(m -> m.getPayload().toString().toUpperCase());

this.mockIntegrationContext.substituteMessageHandlerFor("myService.serviceActivator",
                               mockMessageHandler);
GenericMessage<String> message = new GenericMessage<>("foo");
this.myChannel.send(message);
Message<?> received = this.results.receive(10000);
assertNotNull(received);
assertEquals("FOO", received.getPayload());
assertSame(message, messageArgumentCaptor.getValue());
即使对于带有 ReactiveMessageHandler 配置的 ReactiveStreamsConsumer,也必须使用常规的 MessageHandler 模拟(或 MockMessageHandler)。

有关更多信息,请参阅 MockIntegrationMockMessageHandler Javadoc。

其他资源

除了探索框架本身的测试用例外,Spring Integration Samples 存储库还包含一些专门用于展示测试的示例应用程序,例如 testing-examplesadvanced-testing-examples。在某些情况下,这些示例本身具有全面的端到端测试,例如 file-split-ftp 示例。

© . This site is unofficial and not affiliated with VMware.