跳到主要内容
版本:7.0.2

测试支持

DeepSeek V3 中英对照 Testing support

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 Framework 对 EIP 的规范实现及其核心组件(如 MessageChannel、Endpoint 和 MessageHandler)、抽象层与松耦合原则,您可以实现任意复杂度的集成解决方案。通过使用 Spring Integration API 定义流程,您能够改进、修改甚至替换流程中的某些部分,而(通常)不会影响集成解决方案中的其他组件。然而,测试此类集成解决方案仍然是一个挑战,无论是从端到端方法还是从隔离测试的角度来看。现有的一些工具可以帮助测试或模拟某些集成协议,并且它们能与 Spring Integration 通道适配器良好协作。这类工具的示例如下:

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

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

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

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

  • Tomcat 和 Jetty 提供嵌入式库,用于测试真实的 HTTP、Web 服务或 WebSocket。

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

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

  • Curator Framework 提供了用于 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测试框架。由于Spring Integration基于Spring框架基础,我们在测试集成流时,Spring测试框架的所有功能同样适用。org.springframework.integration.test.context包提供了一些组件,用于增强测试上下文以满足集成需求。首先,我们使用@SpringIntegrationTest注解配置测试类以启用Spring Integration测试框架,如下例所示:

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

@Autowired
private MockIntegrationContext mockIntegrationContext;

}

@SpringIntegrationTest 注解会初始化一个 MockIntegrationContext bean,你可以将其自动装配到测试类中,以访问其方法。通过 noAutoStartup 选项,Spring Integration 测试框架会阻止通常设置为 autoStartup=true 的端点启动。端点会与提供的模式进行匹配,这些模式支持以下简单模式样式:xxx*、**xxx**、*xxx 和 xxx*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 框架。(当前 Mockito 的传递依赖版本为 2.5.x 或更高。)

MockIntegration​

MockIntegration 工厂提供了构建 Spring Integration 流中组件(MessageSource、MessageProducer、MessageHandler 和 MessageChannel)模拟对象的 API。你可以在配置阶段以及目标测试方法中使用这些目标模拟对象,在执行验证和断言之前替换真实端点,如下例所示:

<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 的 Answer 或 doReturn() 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)。

更多信息请参阅 MockIntegration 和 MockMessageHandler 的 Javadoc。

其他资源​

除了探索框架本身的测试案例,Spring Integration 示例仓库 还提供了一些专门用于展示测试的示例应用,例如 testing-examples 和 advanced-testing-examples。在某些情况下,这些示例本身包含了全面的端到端测试,例如 file-split-ftp 示例。