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

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

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

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

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

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

  • Apache Mina 项目中的 FtpServerSshServer 可用于测试 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***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 框架。(当前 Mockito 的传递依赖版本为 2.5.x 或更高。)

MockIntegration

MockIntegration 工厂提供了构建 Spring Integration 流中组件(MessageSourceMessageProducerMessageHandlerMessageChannel)模拟对象的 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 的 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());
备注

即使对于配置了 ReactiveMessageHandlerReactiveStreamsConsumer,也必须使用常规的 MessageHandler 模拟(或 MockMessageHandler)。

更多信息请参阅 MockIntegrationMockMessageHandler 的 Javadoc。

其他资源

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