测试支持

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 框架提供了一个 TestingServer 用于与 Zookeeper 交互。

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

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

大多数这些工具和库都用于 Spring 集成测试。此外,从 GitHub 仓库(在每个模块的 test 目录中),您可以发现有关如何构建自己的集成解决方案测试的想法。

本章的其余部分介绍了 Spring 集成提供的测试工具和实用程序。

测试实用程序

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 集成环境生成 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 Test Framework。由于 Spring Integration 基于 Spring Framework 基础,因此我们在测试集成流时,可以执行 Spring Test Framework 的所有操作。org.springframework.integration.test.context 包提供了一些组件,用于增强集成需求的测试上下文。首先,我们使用@SpringIntegrationTest 注解配置测试类,以启用 Spring Integration Test Framework,如下例所示

@SpringJUnitConfig
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {

    @Autowired
    private MockIntegrationContext mockIntegrationContext;

}

@SpringIntegrationTest 注解会填充一个MockIntegrationContext bean,您可以将其自动装配到测试类中以访问其方法。使用noAutoStartup 选项,Spring Integration Test Framework 会阻止通常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 建议的行为是立即安排轮询任务,并且只执行一次。

有关更多信息,请参阅 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 示例存储库 还有一些专门用于展示测试的示例应用程序,例如 testing-examplesadvanced-testing-examples。在某些情况下,示例本身具有全面的端到端测试,例如 file-split-ftp 示例。