測試支援

Spring Integration 提供了許多實用工具和註解來幫助你測試你的應用程式。測試支援由兩個模組提供

  • spring-integration-test-support 包含核心專案和共享實用工具

  • spring-integration-test 為整合測試提供模擬和應用程式上下文配置元件

spring-integration-test-support(在 5.0 版本之前是 spring-integration-test)為單元測試提供基礎的、獨立的實用工具、規則和匹配器。(它本身不依賴於 Spring Integration,並在 Framework 測試中內部使用)。spring-integration-test 旨在幫助進行整合測試,並提供全面的高階 API 來模擬整合元件並驗證單個元件的行為,包括整個整合流或僅其部分。

本參考手冊的範圍之外是對企業測試的全面論述。請參閱 Gregor Hohpe 和 Wendy Istvanick 撰寫的“企業整合專案中的測試驅動開發”論文,以獲取測試目標整合解決方案的想法和原則來源。

Spring Integration 測試框架和測試實用工具完全基於現有的 JUnit、Hamcrest 和 Mockito 庫。應用程式上下文互動基於Spring 測試框架。有關這些專案的更多資訊,請參閱其文件。

得益於 Spring Integration Framework 中 EIP 的規範實現及其一流公民(如 MessageChannelEndpointMessageHandler)、抽象以及松耦合原則,你可以實現任何複雜度的整合解決方案。利用 Spring Integration 用於流定義的 API,你可以改進、修改甚至替換流中的某些部分,而不會(大部分)影響整合解決方案中的其他元件。測試這樣的整合解決方案仍然是一個挑戰,無論從端到端方法還是從隔離方法來看。一些現有工具可以幫助測試或模擬一些整合協議,並且它們與 Spring Integration 通道介面卡配合良好。這類工具的示例包括以下內容:

  • Spring MockMVC 及其 MockRestServiceServer 可用於測試 HTTP。

  • 一些 RDBMS 供應商提供嵌入式資料庫以支援 JDBC 或 JPA。

  • ActiveMQ 可以嵌入用於測試 JMS 或 STOMP 協議。

  • 有用於嵌入式 MongoDB 和 Redis 的工具。

  • Tomcat 和 Jetty 具有用於測試真實 HTTP、Web Services 或 WebSockets 的嵌入式庫。

  • Apache Mina 專案的 FtpServerSshServer 可用於測試 FTP 和 SFTP 協議。

  • Hazelcast 可以在測試中作為真實資料網格節點執行。

  • Curator Framework 為 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 例項。

有關此類的更多資訊,請參閱其他 TestUtils 方法的Javadoc

使用 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 Rules 和 條件

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 Framework。(當前的 Mockito 傳遞依賴版本為 2.5.x 或更高)。

MockIntegration

MockIntegration 工廠提供了一個 API,用於為作為整合流一部分的 Spring Integration 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 Configuration 來實現與上例相同的配置:

@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<?>, ?>) 用於對下一個請求訊息執行相同的存根邏輯併為其生成回覆。它們可以鏈式呼叫,以模擬所有預期請求訊息變體的任意請求-回覆場景。這些 consumer 和 function 會按順序應用於傳入訊息,直到最後一個,然後該最後一個用於所有剩餘訊息。這種行為類似於 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 示例。