测试支持

Spring 集成提供了许多实用程序和注释来帮助您测试应用程序。 测试支持由两个模块提供:spring-doc.cadn.net.cn

spring-integration-test-support (spring-integration-test在 5.0 之前的版本中)为单元测试提供基本的独立实用程序、规则和匹配器。 (它也不依赖于 Spring 集成本身,并在 Framework 测试内部使用)。spring-integration-test旨在帮助进行集成测试,并提供全面的高级 API 来模拟集成组件并验证单个组件的行为,包括整个集成流或仅部分集成流。spring-doc.cadn.net.cn

对企业中测试的全面处理超出了本参考手册的范围。 请参阅 Gregor Hohpe 和 Wendy Istvanick 撰写的 “Test-Driven Development in Enterprise Integration Projects” 论文,了解测试目标集成解决方案的思路和原则。spring-doc.cadn.net.cn

Spring 集成测试框架和测试工具完全基于现有的 JUnit、Hamcrest 和 Mockito 库。 应用程序上下文交互基于 Spring 测试框架。 有关更多信息,请参阅这些项目的文档。spring-doc.cadn.net.cn

感谢 Spring Integration Framework 中 EIP 的规范实现及其一等公民(例如MessageChannel,EndpointMessageHandler)、抽象和松散耦合原则,您可以实施任何复杂程度的集成解决方案。 使用流定义的 Spring 集成 API,您可以改进、修改甚至替换流的某些部分,而不会影响(大部分)集成解决方案中的其他组件。 测试这样的集成解决方案仍然是一个挑战,无论是从端到端方法还是从隔离方法。 一些现有的工具可以帮助测试或模拟一些集成协议,并且它们与 Spring 集成通道适配器配合得很好。 此类工具的示例包括:spring-doc.cadn.net.cn

这些工具和库中的大多数都用于 Spring 集成测试。 此外,从 GitHub 存储库(在test目录中),您可以发现有关如何为集成解决方案构建自己的测试的想法。spring-doc.cadn.net.cn

本章的其余部分描述了 Spring 集成提供的测试工具和实用程序。spring-doc.cadn.net.cn

测试实用程序

spring-integration-test-supportmodule 为单元测试提供 Utilities 和 Helpers。spring-doc.cadn.net.cn

TestUtils

TestUtilsclass 主要用于 JUnit 测试中的属性断言,如下例所示:spring-doc.cadn.net.cn

@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 私有属性获取值的功能。 如前面的示例所示,它还支持使用点分表示法访问嵌套属性。spring-doc.cadn.net.cn

createTestApplicationContext()Factory 方法会生成一个TestApplicationContext实例。spring-doc.cadn.net.cn

请参阅其他TestUtils方法,了解有关此类的更多信息。spring-doc.cadn.net.cn

OnlyOnceTrigger

OnlyOnceTrigger当您只需要生成一条测试消息并验证行为而不影响其他期间消息时,对于轮询终端节点非常有用。 以下示例显示如何配置OnlyOnceTrigger:spring-doc.cadn.net.cn

<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" />

<int:poller id="jpaPoller" trigger="testTrigger">
    <int:transactional transaction-manager="transactionManager" />
</int:poller>

以下示例演示如何使用前面的OnlyOnceTrigger用于测试:spring-doc.cadn.net.cn

@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 规则和条件

LongRunningIntegrationTestJUnit 4 测试规则用于指示是否应在以下情况下运行 testRUN_LONG_INTEGRATION_TESTSenvironment 或 system 属性设置为true. 否则,将跳过该选项。 出于同样的原因,从 5.1 版本开始,@LongRunningTest为 JUnit 5 测试提供了条件注释。spring-doc.cadn.net.cn

Hamcrest 和 Mockito 匹配器

org.springframework.integration.test.matcher包包含多个Matcher实现来断言Message及其属性。 以下示例演示如何使用一个这样的匹配器 (PayloadMatcher):spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

@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.spring-doc.cadn.net.cn

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).spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

@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():spring-doc.cadn.net.cn

@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.spring-doc.cadn.net.cn

See the Javadoc for more information.spring-doc.cadn.net.cn

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.)spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

<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:spring-doc.cadn.net.cn

@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:spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

The MockIntegrationContext provides a substituteMessageHandlerFor() API that lets you replace the actual configured MessageHandler with a MockMessageHandler in the endpoint under test.spring-doc.cadn.net.cn

The following example shows a typical usage scenario:spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn