测试支持
Spring 集成提供了许多实用程序和注释来帮助您测试应用程序。 测试支持由两个模块提供:
-
spring-integration-test-support
包含核心项和共享实用程序 -
spring-integration-test
为集成测试提供 mocking 和 application context configuration 组件
spring-integration-test-support
(spring-integration-test
在 5.0 之前的版本中)为单元测试提供基本的独立实用程序、规则和匹配器。
(它也不依赖于 Spring 集成本身,并在 Framework 测试内部使用)。spring-integration-test
旨在帮助进行集成测试,并提供全面的高级 API 来模拟集成组件并验证单个组件的行为,包括整个集成流或仅部分集成流。
对企业中测试的全面处理超出了本参考手册的范围。 请参阅 Gregor Hohpe 和 Wendy Istvanick 撰写的 “Test-Driven Development in Enterprise Integration Projects” 论文,了解测试目标集成解决方案的思路和原则。
Spring 集成测试框架和测试工具完全基于现有的 JUnit、Hamcrest 和 Mockito 库。 应用程序上下文交互基于 Spring 测试框架。 有关更多信息,请参阅这些项目的文档。
感谢 Spring Integration Framework 中 EIP 的规范实现及其一等公民(例如MessageChannel
,Endpoint
和MessageHandler
)、抽象和松散耦合原则,您可以实施任何复杂程度的集成解决方案。
使用流定义的 Spring 集成 API,您可以改进、修改甚至替换流的某些部分,而不会影响(大部分)集成解决方案中的其他组件。
测试这样的集成解决方案仍然是一个挑战,无论是从端到端方法还是从隔离方法。
一些现有的工具可以帮助测试或模拟一些集成协议,并且它们与 Spring 集成通道适配器配合得很好。
此类工具的示例包括:
-
Spring
MockMVC
及其MockRestServiceServer
可用于测试 HTTP。 -
一些 RDBMS 供应商提供用于 JDBC 或 JPA 支持的嵌入式数据库。
-
可以嵌入 ActiveMQ 以测试 JMS 或 STOMP 协议。
-
有用于嵌入式 MongoDB 和 Redis 的工具。
-
Tomcat 和 Jetty 具有嵌入式库,用于测试真实的 HTTP、Web 服务或 WebSockets。
-
这
FtpServer
和SshServer
可用于测试 FTP 和 SFTP 协议。 -
Hazelcast 可以在测试中作为真实数据网格节点运行。
-
Curator Framework 提供了一个
TestingServer
进行 Zookeeper 交互。 -
Apache Kafka 提供了管理工具,用于将 Kafka 代理嵌入到测试中。
-
GreenMail 是一个开源、直观且易于使用的电子邮件服务器测试套件,用于测试目的。
这些工具和库中的大多数都用于 Spring 集成测试。
此外,从 GitHub 存储库(在test
目录中),您可以发现有关如何为集成解决方案构建自己的测试的想法。
本章的其余部分描述了 Spring 集成提供的测试工具和实用程序。
测试实用程序
这spring-integration-test-support
module 为单元测试提供 Utilities 和 Helpers。
TestUtils
这TestUtils
class 主要用于 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
并提供从 Target 私有属性获取值的功能。
如前面的示例所示,它还支持使用点分表示法访问嵌套属性。
这createTestApplicationContext()
Factory 方法会生成一个TestApplicationContext
实例。
用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 规则和条件
这LongRunningIntegrationTest
JUnit 4 测试规则用于指示是否应在以下情况下运行 testRUN_LONG_INTEGRATION_TESTS
environment 或 system 属性设置为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)));
}
The MockitoMessageMatchers
factory can be used for mocks for stubbing and verifications, as the following example shows:
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 conditions and predicates
Starting with version 5.2, the MessagePredicate
is introduced to be used in the AssertJ matches()
assertion.
It requires a Message
object as an expectation.
And also ot can be configured with headers to exclude from expectation as well as from actual message to assert.
Spring Integration and the Test Context
Typically, tests for Spring applications use the Spring Test Framework.
Since Spring Integration is based on the Spring Framework foundation, everything we can do with the Spring Test Framework also applies when testing integration flows.
The org.springframework.integration.test.context
package provides some components for enhancing the test context for integration needs.
First we configure our test class with a @SpringIntegrationTest
annotation to enable the Spring Integration Test Framework, as the following example shows:
@SpringJUnitConfig
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {
@Autowired
private MockIntegrationContext mockIntegrationContext;
}
The @SpringIntegrationTest
annotation populates a MockIntegrationContext
bean, which you can autowire to the test class to access its methods.
With the noAutoStartup
option, the Spring Integration Test Framework prevents endpoints that are normally autoStartup=true
from starting.
The endpoints are matched to the provided patterns, which support the following simple pattern styles: xxx*
, xxx
, *xxx
, and xxx*yyy
.
This is useful when we would like to not have real connections to the target systems from inbound channel adapters (for example an AMQP Inbound Gateway, JDBC Polling Channel Adapter, WebSocket Message Producer in client mode, and so on).
The @SpringIntegrationTest
honors the org.springframework.test.context.NestedTestConfiguration
semantics, hence it can be declared on the outer class (or even its super class) - and @SpringIntegrationTest
environment will be available to inherited @Nested
tests.
The MockIntegrationContext
is meant to be used in the target test cases for modifications to beans in the real application context.
For example, endpoints that have autoStartup
overridden to false
can be replaced with mocks, as the following example shows:
@Test
public void testMockMessageSource() {
MessageSource<String> messageSource = () -> new GenericMessage<>("foo");
this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", messageSource);
Message<?> receive = this.results.receive(10_000);
assertNotNull(receive);
}
The mySourceEndpoint
refers here to the bean name of the SourcePollingChannelAdapter
for which we replace the real MessageSource
with our mock.
Similarly, the MockIntegrationContext.substituteMessageHandlerFor()
expects a bean name for the IntegrationConsumer
, which wraps a MessageHandler
as an endpoint.
After test is performed you can restore the state of endpoint beans to the real configuration using MockIntegrationContext.resetBeans()
:
@After
public void tearDown() {
this.mockIntegrationContext.resetBeans();
}
Starting with version 6.3, the MockIntegrationContext.substituteTriggerFor()
API has been introduced.
This can be used to replace the real Trigger
in the AbstractPollingEndpoint
.
For example the production configuration may rely on daily (or even weekly) cron schedule.
Any custom Trigger
can be injected into the target endpoint to mitigate the time span.
For example, the mentioned above OnlyOnceTrigger
suggests a behavior to schedule polling task immediately and do that only once.
See the Javadoc for more information.
Integration Mocks
The org.springframework.integration.test.mock
package offers tools and utilities for mocking, stubbing, and verification of activity on Spring Integration components.
The mocking functionality is fully based on and compatible with the well known Mockito Framework.
(The current Mockito transitive dependency is on version 2.5.x or higher.)
MockIntegration
The MockIntegration
factory provides an API to build mocks for Spring Integration beans that are parts of the integration flow (MessageSource
, MessageProducer
, MessageHandler
, and MessageChannel
).
You can use the target mocks during the configuration phase as well as in the target test method to replace the real endpoints before performing verifications and assertions, as the following example shows:
<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>
The following example shows how to use Java Configuration to achieve the same configuration as the preceding example:
@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();
For this purpose, the aforementioned MockIntegrationContext
should be used from the test, as the following example shows:
this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint",
MockIntegration.mockMessageSource("foo", "bar", "baz"));
Message<?> receive = this.results.receive(10_000);
assertNotNull(receive);
assertEquals("FOO", receive.getPayload());
Unlike the Mockito MessageSource
mock object, the MockMessageHandler
is a regular AbstractMessageProducingHandler
extension with a chain API to stub handling for incoming messages.
The MockMessageHandler
provides handleNext(Consumer<Message<?>>)
to specify a one-way stub for the next request message.
It is used to mock message handlers that do not produce replies.
The handleNextAndReply(Function<Message<?>, ?>)
is provided for performing the same stub logic for the next request message and producing a reply for it.
They can be chained to simulate any arbitrary request-reply scenarios for all expected request messages variants.
These consumers and functions are applied to the incoming messages, one at a time from the stack, until the last, which is then used for all remaining messages.
The behavior is similar to the Mockito Answer
or doReturn()
API.
In addition, you can supply a Mockito ArgumentCaptor<Message<?>>
to the MockMessageHandler
in a constructor argument.
Each request message for the MockMessageHandler
is captured by that ArgumentCaptor
.
During the test, you can use its getValue()
and getAllValues()
methods to verify and assert those request messages.
The MockIntegrationContext
provides a substituteMessageHandlerFor()
API that lets you replace the actual configured MessageHandler
with a MockMessageHandler
in the endpoint under test.
The following example shows a typical usage scenario:
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());
The regular MessageHandler
mocking (or MockMessageHandler
) has to be used even for a ReactiveStreamsConsumer
with a ReactiveMessageHandler
configuration.
See the MockIntegration
and MockMessageHandler
Javadoc for more information.
Other Resources
As well as exploring the test cases in the framework itself, the Spring Integration Samples repository has some sample applications specifically made to show testing, such as testing-examples
and advanced-testing-examples
.
In some cases, the samples themselves have comprehensive end-to-end tests, such as the file-split-ftp
sample.