测试支持
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
示例。