测试支持

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 撰写的 “企业集成项目中的测试驱动开发” 论文。

Spring Integration 测试框架和测试实用程序完全基于现有的 JUnit、Hamcrest 和 Mockito 库。应用程序上下文交互基于 Spring 测试框架。有关更多信息,请参阅这些项目的文档。

由于 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 服务或 WebSockets。

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

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

  • Curator 框架为 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 实例。

有关此类的更多信息,请参阅 Javadoc 中的其他 TestUtils 方法。

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

支持组件

org.springframework.integration.test.support 包含各种抽象类,您应该在目标测试中实现这些类

JUnit 规则和条件

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

AssertJ 条件和谓词

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

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*xxxxxx*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 框架,并与之兼容。(当前 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 是一个常规的 AbstractMessageProducingHandler 扩展,它具有一个链式 API 用于存根传入消息的处理。MockMessageHandler 提供 handleNext(Consumer<Message<?>>) 用于指定下一个请求消息的单向存根。它用于模拟不产生回复的消息处理器。handleNextAndReply(Function<Message<?>, ?>) 用于对下一个请求消息执行相同的存根逻辑并为其生成回复。它们可以链接起来,模拟所有预期请求消息变体的任意请求-回复场景。这些消费者和函数逐个应用于传入的消息,直到最后一个,然后将其用于所有剩余的消息。这种行为类似于 Mockito 的 AnswerdoReturn() 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)。

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

其他资源

除了探索框架本身的测试用例之外,Spring Integration Samples 存储库 还提供了一些专门用于展示测试的示例应用程序,例如 testing-examplesadvanced-testing-examples。在某些情况下,示例本身包含全面的端到端测试,例如 file-split-ftp 示例。