测试支持
Spring Integration 提供了许多实用工具和注解来帮助你测试你的应用程序。测试支持由两个模块提供
-
spring-integration-test-support包含核心项目和共享实用工具 -
spring-integration-test为集成测试提供模拟和应用程序上下文配置组件
spring-integration-test-support(在 5.0 版本之前是 spring-integration-test)为单元测试提供基础的、独立的实用工具、规则和匹配器。(它本身不依赖于 Spring Integration,并在 Framework 测试中内部使用)。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 Services 或 WebSockets 的嵌入式库。
-
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();
...
}
JUnit Rules 和 条件
LongRunningIntegrationTest JUnit 4 测试规则用于指示当 RUN_LONG_INTEGRATION_TESTS 环境变量或系统属性设置为 true 时是否应运行测试。否则,它将被跳过。出于同样的原因,自 5.1 版本起,为 JUnit 5 测试提供了 @LongRunningTest 条件注解。
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));
Spring Integration 与测试上下文
通常,Spring 应用程序的测试使用 Spring 测试框架。由于 Spring Integration 基于 Spring Framework 基础,因此使用 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 的状态恢复到实际配置。
@After
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(MessageSource、MessageProducer、MessageHandler 和 MessageChannel)构建模拟对象。你可以在配置阶段以及目标测试方法中使用目标模拟对象,以便在执行验证和断言之前替换实际端点,如下例所示:
<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 Configuration 来实现与上例相同的配置:
@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<?>, ?>) 用于对下一个请求消息执行相同的存根逻辑并为其生成回复。它们可以链式调用,以模拟所有预期请求消息变体的任意请求-回复场景。这些 consumer 和 function 会按顺序应用于传入消息,直到最后一个,然后该最后一个用于所有剩余消息。这种行为类似于 Mockito 的 Answer 或 doReturn() API。
此外,你可以在构造函数参数中向 MockMessageHandler 提供一个 Mockito ArgumentCaptor<Message<?>>。MockMessageHandler 的每个请求消息都由该 ArgumentCaptor 捕获。在测试期间,你可以使用其 getValue() 和 getAllValues() 方法来验证和断言这些请求消息。
MockIntegrationContext 提供 substituteMessageHandlerFor() API,允许你在被测端点中用 MockMessageHandler 替换实际配置的 MessageHandler。
以下示例展示了一个典型的使用场景:
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 Samples 仓库还包含一些专门用于展示测试的示例应用程序,例如 testing-examples 和 advanced-testing-examples。在某些情况下,这些示例本身就包含全面的端到端测试,例如 file-split-ftp 示例。