跳到主要内容

测试支持

QWen Plus 中英对照 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 集成测试框架和测试工具完全基于现有的 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 服务或 WebSockets。

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

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

  • Curator Framework 提供了一个 TestingServer 用于 Zookeeper 交互。

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

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

这些工具和库大多用于 Spring 集成测试。此外,您还可以从 GitHub 仓库(在每个模块的 test 目录中)中发现如何为集成解决方案构建自己的测试的想法。

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

测试工具

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

测试工具

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

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

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>
xml

以下示例展示了如何使用前述的 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();
...
}
java

支持组件

org.springframework.integration.test.support 包包含各种抽象类,你应该在目标测试中实现这些抽象类

JUnit 规则和条件

LongRunningIntegrationTest JUnit 4 测试规则用于指示如果 RUN_LONG_INTEGRATION_TESTS 环境或系统属性设置为 true 时,测试是否应该运行。否则,它将被跳过。出于同样的原因,自 5.1 版本起,提供了一个 @LongRunningTest 条件注解用于 JUnit 5 测试。

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

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));
java

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;

}
java

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

当我们将入站通道适配器(例如 AMQP 入站网关、JDBC 轮询通道适配器、客户端模式的 WebSocket 消息生产者等)与目标系统的真实连接断开时,这非常有用。

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

MockIntegrationContext 旨在用于针对修改真实应用程序上下文中 beans 的目标测试用例。例如,端点的 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);
}
java
备注

mySourceEndpoint 在这里指的是 SourcePollingChannelAdapter 的 bean 名称,我们用 mock 替换其真实的 MessageSource 。类似地,MockIntegrationContext.substituteMessageHandlerFor() 期望一个 IntegrationConsumer 的 bean 名称,该消费者将 MessageHandler 包装为一个端点。

测试完成后,您可以使用 MockIntegrationContext.resetBeans() 将端点 Bean 的状态恢复到实际配置:

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

从 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 工厂提供了一个 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>
xml

以下示例展示了如何使用 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();
java

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

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

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

此外,你可以提供一个 Mockito ArgumentCaptor<Message<?>>MockMessageHandler 作为构造函数参数。每个发送给 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());
java
备注

即使是对于带有 ReactiveMessageHandler 配置的 ReactiveStreamsConsumer,也必须使用常规的 MessageHandler 模拟(或 MockMessageHandler)。

请参阅 MockIntegrationMockMessageHandler Javadoc 以获取更多信息。

其他资源

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