# 测试支持

## 测试支持

Spring 集成提供了许多实用工具和注释，以帮助你测试应用程序。测试支持由两个模块提供：

* `spring-integration-test-support`包含核心项和共享实用程序

* `spring-integration-test`为集成测试提供模拟和应用程序上下文配置组件

`spring-integration-test-support`（`spring-integration-test`在 5.0 之前的版本中）为单元测试提供了基本的、独立的实用工具、规则和匹配器。（它也不依赖于 Spring 集成本身，并在框架测试中内部使用）。`spring-integration-test`旨在帮助集成测试，并提供一个全面的高级 API 来模拟集成组件并验证单个组件的行为，包括整个集成流或仅其中的一部分。

在 Enterprise 中对测试的彻底处理超出了本参考手册的范围。请参阅 Gregor Hohpe 和 Wendy Istvanick 的[“Enterprise 集成项目中的测试驱动开发”](https://www.enterpriseintegrationpatterns.com/docs/TestDrivenEAI.pdf)论文，以获取测试目标集成解决方案的想法和原则的来源。

Spring 集成测试框架和测试实用程序完全基于现有的 JUnit、Hamcrest 和 Mockito 库。应用程序上下文交互基于[Spring test framework](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html#testing)。有关更多信息，请参见这些项目的文档。

由于 Spring 集成框架中 EIP 的规范实现及其第一类公民（例如`MessageChannel`，`Endpoint`和`MessageHandler`）、抽象和松耦合原则，你可以实现任何复杂性的集成解决方案。通过 Spring 流定义的集成 API，你可以改进、修改甚至替换流的某些部分，而不会（主要）影响集成解决方案中的其他组件。测试这样的集成解决方案仍然是一个挑战，无论是从端到端的方法还是从孤立的方法。现有的几个工具可以帮助测试或模拟某些集成协议，并且它们可以很好地与 Spring 集成通道适配器一起工作。这类工具的例子包括：

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

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

* ActiveMQ 可以嵌入用于测试 JMS 或 STOMP 协议。

* 有用于嵌入式 MongoDB 和 Redis 的工具。

* Tomcat 和 Jetty 具有嵌入式库来测试真正的 HTTP、Web 服务或 WebSockets。

* 来自 Apache Mina 项目的`FtpServer`和`SshServer`可用于测试 FTP 和 SFTP 协议。

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

* Curator 框架为 ZooKeeper 交互提供了`TestingServer`。

* Apache Kafka 提供了在测试中嵌入 Kafka 代理的管理工具。

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

这些工具和库中的大多数都用于 Spring 集成测试。此外，从 GitHub[repository](https://github.com/spring-projects/spring-integration)（在每个模块的`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()`Factory 方法使用所提供的 Spring 集成环境生成`TestApplicationContext`实例。

有关此类的更多信息，请参见其他`TestUtils`方法的[Javadoc](https://docs.spring.io/spring-integration/api/org/springframework/integration/test/util/TestUtils.html)。

#### 使用`OnlyOnceTrigger`

[`OnlyOnceTrigger`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/util/onlyoncetrigger.html）对于只需要生成一条测试消息并验证行为而不影响其他周期消息的轮询端点是有用的。下面的示例展示了如何配置`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`包包含你应该在目标测试中实现的各种抽象类

* [`AbstractRequestResponseScenarioTests`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/abstractrequestresponsescenariotestes.html）

* [`AbstractResponseValidator`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/abstractresponsevalidator.html）

* [`LogAdjustingTestSupport`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/logAdjustingTestSupport.html）（弃用）

* [`MessageValidator`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/messagevalidator.html）

* [`PayloadValidator`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/payloadvalidator.html）

* [`RequestResponseScenario`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/requestresponsescenario.html）

* [`SingleRequestResponseScenarioTests`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/support/singleRequestResponsescenarioTests.html）

#### JUnit 规则和条件

存在`LongRunningIntegrationTest`JUnit4 测试规则来指示如果`RUN_LONG_INTEGRATION_TESTS`环境或系统属性设置为`true`是否应该运行测试。否则就跳过了。出于与版本 5.1 相同的原因，为 JUnit5 测试提供了`@LongRunningTest`条件注释。

#### Hamcrest 和 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 条件和谓词

从版本 5.2 开始，引入了`MessagePredicate`来在 assertj`matches()`断言中使用。它需要一个`Message`对象作为期望。OT 也可以通过配置头来排除期望消息和要断言的实际消息。

###  Spring 集成和测试上下文

通常，针对 Spring 应用程序的测试使用 Spring 测试框架。由于 Spring 集成是基于 Spring 框架基础的，因此我们可以使用 Spring 测试框架所做的一切也适用于测试集成流时。`org.springframework.integration.test.context`包为增强集成需求的测试上下文提供了一些组件。首先，我们使用`@SpringIntegrationTest`注释配置我们的测试类，以启用 Spring 集成测试框架，如下例所示：

```
@RunWith(SpringRunner.class)
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {

    @Autowired
    private MockIntegrationContext mockIntegrationContext;

}
```

`@SpringIntegrationTest`注释填充`MockIntegrationContext` Bean，你可以将其自动连接到测试类以访问其方法。使用`noAutoStartup`选项， Spring 集成测试框架可以阻止通常`autoStartup=true`的端点启动。端点与所提供的模式匹配，这些模式支持以下简单的模式样式：`xxx*`，`**xxx**`**，`*xxx`**，和`xxx*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 名，我们用我们的模拟替换了真正的`MessageSource`。同样的，`MockIntegrationContext.substituteMessageHandlerFor()`期望`IntegrationConsumer`的 Bean 名，它将`MessageHandler`包装为一个端点。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在执行测试之后，你可以使用`MockIntegrationContext.resetBeans()`将端点 bean 的状态恢复到实际配置：

```
@After
public void tearDown() {
    this.mockIntegrationContext.resetBeans();
}
```

有关更多信息，请参见[Javadoc](https://docs.spring.io/spring-integration/api/org/springframework/integration/test/context/MockIntegrationContext.html)。

### 集成模拟

`org.springframework.integration.test.mock`包提供了用于模拟、存根和验证 Spring 集成组件上的活动的工具和实用程序。模拟功能完全基于并兼容众所周知的 Mockito 框架。（当前的 Mockito 传递依赖项位于版本 2.5.x 或更高版本。

#### 模拟积分

`MockIntegration`工厂提供了一个 API 来为 Spring 集成 bean 构建模拟，这些集成 bean 是集成流的一部分（`MessageSource`，`MessageProducer`，`MessageHandler`和`MessageChannel`）。在执行验证和断言之前，可以在配置阶段以及在目标测试方法中使用目标模拟来替换实际的端点，如下例所示：

```
<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 = IntegrationFlows
        .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`Mock 对象不同，`MockMessageHandler`是一个常规的`AbstractMessageProducingHandler`扩展，具有用于对传入消息进行存根处理的链 API。`MockMessageHandler`提供`handleNext(Consumer<Message<?>>)`来为下一个请求消息指定单向存根。它用于模拟不产生回复的消息处理程序。提供了`handleNextAndReply(Function<Message<?>, ?>)`，用于为下一个请求消息执行相同的存根逻辑，并为其生成应答。可以将它们链接起来，以模拟所有预期的请求消息变体的任意请求-回复场景。这些消费者和函数被应用到传入消息上，从堆栈一次一个，直到最后一个，然后用于所有剩余的消息。该行为类似于 mockito`Answer`或`doReturn()`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());
```

参见[`MockIntegration`]（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/test/mock/mockintegration.html）和[`MockMessageHandler`（https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/integration/test/mock/mockmessagehandler.html）Javadoc 以获取更多信息。

### 其他资源

除了在框架本身中探索测试用例之外，[Spring Integration Samples repository](https://github.com/spring-projects/spring-integration-samples)还有一些专门用于显示测试的示例应用程序，例如`testing-examples`和`advanced-testing-examples`。在某些情况下，样本本身具有全面的端到端测试，例如`file-split-ftp`样本。