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 撰写的 “Test-Driven Development in Enterprise Integration Projects” 论文,获取有关如何测试您的目标集成解决方案的想法和原则。 Spring Integration 测试框架和测试实用程序完全基于现有的 JUnit、Hamcrest 和 Mockito 库。应用程序上下文交互基于 Spring test framework。请参阅这些项目的文档以获取更多信息。 由于 Spring Integration 框架中 EIP 和其一类公民(如 MessageChannelEndpointMessageHandler)的规范实现、抽象以及松散耦合原则,你可以实现任何复杂性的集成解决方案。使用 Spring Integration API 进行流定义,你可以在不影响(大部分)集成解决方案中其他组件的情况下,对流程的某个部分进行改进、修改甚至替换。测试此类集成解决方案仍然是一个挑战,无论是从端到端方法还是隔离方法。一些现有的工具可以帮助测试或模拟某些集成协议,并且它们与 Spring Integration 通道适配器配合得很好。这些工具的示例包括:

  • Spring MockMVC 及其 MockRestServiceServer 可用于测试 HTTP。

  • 一些 RDBMS 供应商提供 JDBC 或 JPA 支持的嵌入式数据库。

  • ActiveMQ 可用于测试 JMS 或 STOMP 协议。

  • 有一些用于嵌入式 MongoDB 和 Redis 的工具。

  • Tomcat 和 Jetty 具有嵌入式库,用于测试真正的 HTTP、Web 服务或 WebSocket。

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

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

  • Curator Framework 框架为 Zookeeper 交互提供了一个 TestingServer

  • Apache Kafka 提供了管理工具,以便在测试中嵌入 Kafka Broker。

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

大多数这些工具和库都用于 Spring Integration 测试。此外,从 GitHub repository(在每个模块的 test 目录中),您可以发现如何针对集成解决方案构建自己的教程。 本章的其余部分描述了 Spring Integration 提供的测试工具和实用工具。

Testing Utilities

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,以获取有关此类的更多信息。

Using 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();
    ...
}

Support Components

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

JUnit Rules and Conditions

如果将测试环境或系统属性 RUN_LONG_INTEGRATION_TESTS 设置为 true,则 JUnit 4 测试规则 LongRunningIntegrationTest 现在指示是否应运行测试。否则,将跳过它。从 5.1 版开始,出于同样的原因,为 JUnit 5 测试提供了 @LongRunningTest 条件注释。

Hamcrest and Mockito Matchers

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

AssertJ conditions and predicates

从 5.2 版开始,引入了 MessagePredicate,可在 AssertJ matches() 断言中使用。它需要一个 Message 对象作为期望。此外,还可以针对排除在期望之外的以及需要断言的实际消息中的标头进行配置。

Spring Integration and the Test Context

通常,用于 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;

}

@SpringIntegrationTest 注解填充了一个 MockIntegrationContext Bean,你可以将其自动连接至测试类以访问其方法。使用 noAutoStartup 选项,Spring Integration 测试框架阻止了通常 autoStartup=true 的端点启动。端点与提供的模式相匹配,支持以下简单模式样式:xxx*, xxx, *xxxxxx*yyy

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

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 名称,我们用 Mock 替换其实际的 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`,using-onlyoncetrigger>> 建议采取一种行为,即立即安排轮询任务并且仅执行一次。

有关更多信息,请参阅 Javadoc

Integration Mocks

org.springframework.integration.test.mock 包为 Spring Integration 组件的活动提供模拟、存根和验证的工具和实用程序。模拟功能完全基于备受认可的 Mockito 框架,并且与此框架兼容。(当前 Mockito 传递依赖项采用 2.5.x 或更高版本。)

MockIntegration

MockIntegration 工厂提供了一个 API 来构建 Spring Integration Bean 的模拟,这些 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>

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

为此,应从测试中使用上述 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 是一个带有链 API 的常规 AbstractMessageProducingHandler 扩展,用于存根传入消息的处理。MockMessageHandler 提供 handleNext(Consumer<Message<?>>) 来为下一个请求消息指定单向存根。它用于模拟不产生回复的消息处理器。handleNextAndReply(Function<Message<?>, ?>) 提供用于对下一个请求消息执行相同存根逻辑并生成其回复。可以对它们进行链接,以模拟针对所有预期请求消息变体的所有任意的请求-回复场景。这些使用者和函数将应用于传入消息(一次一个),知道最后一个使用者或函数,然后将其用于所有剩余消息。其行为类似于 Mockito AnswerdoReturn() API。

此外,你可以向构造函数参数中的 MockMessageHandler 提供 Mockito ArgumentCaptor<Message<?>>MockMessageHandler 的每个请求消息都会被该 ArgumentCaptor 捕获。在测试期间,你可以使用它的 getValue()getAllValues() 方法验证和断言这些请求消息。

MockIntegrationContext 提供了一个 substituteMessageHandlerFor() API,使用此 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());

即使对于具有 queue 目标的通道适配器,也必须使用正则 mocking(或 dynamic)。

有关更多信息,请参阅 MockIntegrationMockMessageHandler Javadoc。

Other Resources

除了探索框架本身中的测试用例之外, Spring Integration Samples repository 有一些专门用于展示测试的示例应用程序,例如 testing-examplesadvanced-testing-examples。在某些情况下,示例本身具有综合性的端到端测试,例如 file-split-ftp 示例。