訊息傳遞

Spring Cloud Contract 讓您可以驗證使用訊息傳遞作為溝通方式的應用程式。本文檔中顯示的所有整合都適用於 Spring,但您也可以建立自己的整合並使用它。

訊息傳遞 DSL 頂層元素

訊息傳遞的 DSL 看起來與專注於 HTTP 的 DSL 略有不同。以下各節將說明差異之處

由方法觸發的輸出

輸出訊息可以透過呼叫方法(例如,當契約開始且訊息已發送時的 Scheduler)來觸發,如下例所示

Groovy
def dsl = Contract.make {
	// Human readable description
	description 'Some description'
	// Label by means of which the output message can be triggered
	label 'some_label'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('bookReturnedTriggered()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo('output')
		// the body of the output message
		body('''{ "bookName" : "foo" }''')
		// the headers of the output message
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}
YAML
# Human readable description
description: Some description
# Label by means of which the output message can be triggered
label: some_label
input:
  # the contract will be triggered by a method
  triggeredBy: bookReturnedTriggered()
# output message of the contract
outputMessage:
  # destination to which the output message will be sent
  sentTo: output
  # the body of the output message
  body:
    bookName: foo
  # the headers of the output message
  headers:
    BOOK-NAME: foo

在先前的範例案例中,如果呼叫名為 bookReturnedTriggered 的方法,則輸出訊息會傳送到 output。在訊息發佈者端,我們產生一個測試,呼叫該方法以觸發訊息。在消費者端,您可以使用 some_label 來觸發訊息。

消費者/生產者

本節僅適用於 Groovy DSL。

在 HTTP 中,您有 client/stub 和 `server/test 標記法的概念。您也可以在訊息傳遞中使用這些範例。此外,Spring Cloud Contract Verifier 也提供 consumerproducer 方法(請注意,您可以使用 $value 方法來提供 consumerproducer 部分)。

通用

inputoutputMessage 區段中,您可以呼叫 assertThat 並帶入 method 的名稱(例如,assertThatMessageIsOnTheQueue()),該方法已在基底類別或靜態匯入中定義。Spring Cloud Contract 會在產生的測試中執行該方法。

整合

您可以使用以下整合組態之一

  • Apache Camel

  • Spring Integration

  • Spring Cloud Stream

  • Spring JMS

由於我們使用 Spring Boot,如果您已將這些程式庫之一新增至類別路徑,則所有訊息傳遞組態都會自動設定。

請記住在產生的測試的基底類別上放置 @AutoConfigureMessageVerifier。否則,Spring Cloud Contract 的訊息傳遞部分將無法運作。

如果您想要使用 Spring Cloud Stream,請記住新增 org.springframework.cloud:spring-cloud-stream 的測試依賴項,如下所示

Maven
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
    <type>test-jar</type>
    <scope>test</scope>
    <classifier>test-binder</classifier>
</dependency>
Gradle
testImplementation(group: 'org.springframework.cloud', name: 'spring-cloud-stream', classifier: 'test-binder')

手動整合測試

測試使用的主要介面是 org.springframework.cloud.contract.verifier.messaging.MessageVerifierSenderorg.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver。它定義了如何發送和接收訊息。

在測試中,您可以注入 ContractVerifierMessageExchange 以發送和接收遵循契約的訊息。然後將 @AutoConfigureMessageVerifier 新增至您的測試。以下範例顯示如何執行此操作

@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {

  @Autowired
  private MessageVerifier verifier;
  ...
}
如果您的測試也需要 Stub,則 @AutoConfigureStubRunner 會包含訊息傳遞組態,因此您只需要一個註解。

生產者端訊息傳遞測試產生

在您的 DSL 中具有 inputoutputMessage 區段,會在發佈者端建立測試。預設情況下,會建立 JUnit 4 測試。但是,也可以建立 JUnit 5、TestNG 或 Spock 測試。

傳遞至 messageFromsentTo 的目的地對於不同的訊息傳遞實作可能具有不同的含義。對於 Stream 和 Integration,它首先解析為通道的 destination。然後,如果沒有此類 destination,則將其解析為通道名稱。對於 Camel,這是一個特定的元件(例如,jms)。

考慮以下契約

Groovy
def contractDsl = Contract.make {
	name "foo"
	label 'some_label'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('activemq:output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
			messagingContentType(applicationJson())
		}
	}
}
YAML
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

對於前面的範例,將建立以下測試

JUnit
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;

public class FooTest {
	@Inject ContractVerifierMessaging contractVerifierMessaging;
	@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

	@Test
	public void validate_foo() throws Exception {
		// when:
			bookReturnedTriggered();

		// then:
			ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
					contract(this, "foo.yml"));
			assertThat(response).isNotNull();

		// and:
			assertThat(response.getHeader("BOOK-NAME")).isNotNull();
			assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
			assertThat(response.getHeader("contentType")).isNotNull();
			assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");

		// and:
			DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
			assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
	}

}
Spock
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes

class FooSpec extends Specification {
	@Inject ContractVerifierMessaging contractVerifierMessaging
	@Inject ContractVerifierObjectMapper contractVerifierObjectMapper

	def validate_foo() throws Exception {
		when:
			bookReturnedTriggered()

		then:
			ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
					contract(this, "foo.yml"))
			response != null

		and:
			response.getHeader("BOOK-NAME") != null
			response.getHeader("BOOK-NAME").toString() == 'foo'
			response.getHeader("contentType") != null
			response.getHeader("contentType").toString() == 'application/json'

		and:
			DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
			assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
	}

}

消費者 Stub 產生

與 HTTP 部分不同,在訊息傳遞中,我們需要在 JAR 內發佈契約定義以及 Stub。然後在消費者端解析它,並建立適當的 Stub 路由。

如果您在類別路徑上有多個框架,Stub Runner 需要定義應使用哪個框架。假設您的類別路徑上有 AMQP、Spring Cloud Stream 和 Spring Integration,並且您想要使用 Spring AMQP。然後您需要設定 stubrunner.stream.enabled=falsestubrunner.integration.enabled=false。這樣,唯一剩下的框架就是 Spring AMQP。

Stub 觸發

若要觸發訊息,請使用 StubTrigger 介面,如下例所示

import java.util.Collection;
import java.util.Map;

/**
 * Contract for triggering stub messages.
 *
 * @author Marcin Grzejszczak
 */
public interface StubTrigger {

	/**
	 * Triggers an event by a given label for a given {@code groupid:artifactid} notation.
	 * You can use only {@code artifactId} too.
	 *
	 * Feature related to messaging.
	 * @param ivyNotation ivy notation of a stub
	 * @param labelName name of the label to trigger
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String ivyNotation, String labelName);

	/**
	 * Triggers an event by a given label.
	 *
	 * Feature related to messaging.
	 * @param labelName name of the label to trigger
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String labelName);

	/**
	 * Triggers all possible events.
	 *
	 * Feature related to messaging.
	 * @return true - if managed to run a trigger
	 */
	boolean trigger();

	/**
	 * Feature related to messaging.
	 * @return a mapping of ivy notation of a dependency to all the labels it has.
	 */
	Map<String, Collection<String>> labels();

}

為了方便起見,StubFinder 介面擴展了 StubTrigger,因此您的測試中只需要其中一個即可。

StubTrigger 為您提供以下選項來觸發訊息

依標籤觸發

以下範例顯示如何使用標籤觸發訊息

stubFinder.trigger('return_book_1')

依群組和 Artifact ID 觸發

以下範例顯示如何依群組和 Artifact ID 觸發訊息

stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')

依 Artifact ID 觸發

以下範例顯示如何從 Artifact ID 觸發訊息

stubFinder.trigger('streamService', 'return_book_1')

觸發所有訊息

以下範例顯示如何觸發所有訊息

stubFinder.trigger()

搭配 Apache Camel 的消費者端訊息傳遞

Spring Cloud Contract Stub Runner 的訊息傳遞模組讓您可以輕鬆地與 Apache Camel 整合。對於提供的 Artifact,它會自動下載 Stub 並註冊所需的路由。

將 Apache Camel 新增至專案

您可以在類別路徑上同時擁有 Apache Camel 和 Spring Cloud Contract Stub Runner。請記住使用 @AutoConfigureStubRunner 註解您的測試類別。

停用功能

如果您需要停用此功能,請設定 stubrunner.camel.enabled=false 屬性。

範例

假設我們有以下 Maven 儲存庫,其中部署了 camelService 應用程式的 Stub

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── camelService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── camelService-0.0.1-SNAPSHOT.pom
                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

此外,假設 Stub 包含以下結構

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

現在考慮以下契約

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('rabbitmq:output?queue=output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

若要從 return_book_1 標籤觸發訊息,我們使用 StubTrigger 介面,如下所示

stubFinder.trigger("return_book_1")

這將發送訊息到契約的輸出訊息中描述的目的地。

搭配 Spring Integration 的消費者端訊息傳遞

Spring Cloud Contract Stub Runner 的訊息傳遞模組讓您可以輕鬆地與 Spring Integration 整合。對於提供的 Artifact,它會自動下載 Stub 並註冊所需的路由。

將 Runner 新增至專案

您可以在類別路徑上同時擁有 Spring Integration 和 Spring Cloud Contract Stub Runner。請記住使用 @AutoConfigureStubRunner 註解您的測試類別。

停用功能

如果您需要停用此功能,請設定 stubrunner.integration.enabled=false 屬性。

範例

假設您有以下 Maven 儲存庫,其中部署了 integrationService 應用程式的 Stub

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── integrationService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

此外,假設 Stub 包含以下結構

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

考慮以下契約

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

現在考慮以下 Spring Integration 路由

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
			 xmlns:beans="http://www.springframework.org/schema/beans"
			 xmlns="http://www.springframework.org/schema/integration"
			 xsi:schemaLocation="http://www.springframework.org/schema/beans
			https://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/integration
			http://www.springframework.org/schema/integration/spring-integration.xsd">


	<!-- REQUIRED FOR TESTING -->
	<bridge input-channel="output"
			output-channel="outputTest"/>

	<channel id="outputTest">
		<queue/>
	</channel>

</beans:beans>

若要從 return_book_1 標籤觸發訊息,請使用 StubTrigger 介面,如下所示

stubFinder.trigger('return_book_1')

這將發送訊息到契約的輸出訊息中描述的目的地。

搭配 Spring Cloud Stream 的消費者端訊息傳遞

Spring Cloud Contract Stub Runner 的訊息傳遞模組讓您可以輕鬆地與 Spring Stream 整合。對於提供的 Artifact,它會自動下載 Stub 並註冊所需的路由。

如果 Stub Runner 與 Stream 的整合 messageFromsentTo 字串首先解析為通道的 destination,且不存在此類 destination,則目的地會解析為通道名稱。

如果您想要使用 Spring Cloud Stream,請記住新增 org.springframework.cloud:spring-cloud-stream 測試支援的依賴項,如下所示

Maven
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-test-binder</artifactId>
    <scope>test</scope>
</dependency>
Gradle
testImplementation('org.springframework.cloud:spring-cloud-stream-test-binder')

將 Runner 新增至專案

您可以在類別路徑上同時擁有 Spring Cloud Stream 和 Spring Cloud Contract Stub Runner。請記住使用 @AutoConfigureStubRunner 註解您的測試類別。

停用功能

如果您需要停用此功能,請設定 stubrunner.stream.enabled=false 屬性。

範例

假設您有以下 Maven 儲存庫,其中部署了 streamService 應用程式的 Stub

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── streamService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── streamService-0.0.1-SNAPSHOT.pom
                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

此外,假設 Stub 包含以下結構

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

考慮以下契約

Contract.make {
	label 'return_book_1'
	input { triggeredBy('bookReturnedTriggered()') }
	outputMessage {
		sentTo('returnBook')
		body('''{ "bookName" : "foo" }''')
		headers { header('BOOK-NAME', 'foo') }
	}
}

現在考慮以下 Spring Cloud Stream 函數組態

@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
@Configuration(proxyBeanMethods = true)
@EnableAutoConfiguration
protected static class Config {

	@Bean
	Function<String, String> test1() {
		return (input) -> {
			println "Test 1 [${input}]"
			return input
		}
	}

}

現在考慮以下 Spring 組態

stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
stubrunner.stubs-mode: remote
spring:
  cloud:
    stream:
      bindings:
        test1-in-0:
          destination: returnBook
        test1-out-0:
          destination: outputToAssertBook
    function:
      definition: test1

server:
  port: 0

debug: true

若要從 return_book_1 標籤觸發訊息,請使用 StubTrigger 介面,如下所示

stubFinder.trigger('return_book_1')

這將發送訊息到契約的輸出訊息中描述的目的地。

搭配 Spring JMS 的消費者端訊息傳遞

Spring Cloud Contract Stub Runner 的訊息傳遞模組提供了一種輕鬆與 Spring JMS 整合的方式。

此整合假設您有一個正在執行的 JMS Broker 實例。

將 Runner 新增至專案

您需要在類別路徑上同時擁有 Spring JMS 和 Spring Cloud Contract Stub Runner。請記住使用 @AutoConfigureStubRunner 註解您的測試類別。

範例

假設 Stub 結構如下所示

├── stubs
    └── bookReturned1.groovy

此外,假設以下測試組態

stubrunner:
  repository-root: stubs:classpath:/stubs/
  ids: my:stubs
  stubs-mode: remote
spring:
  activemq:
    send-timeout: 1000
  jms:
    template:
      receive-timeout: 1000

現在考慮以下契約

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOKNAME', 'foo')
		}
	}
}

若要從 return_book_1 標籤觸發訊息,我們使用 StubTrigger 介面,如下所示

stubFinder.trigger('return_book_1')

這將發送訊息到契約的輸出訊息中描述的目的地。