メッセージング

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 and `server/test 表記という概念があります。これらのパラダイムはメッセージングにも使用できます。さらに、Spring Cloud Contract Verifier は、consumer および producer メソッドも提供します ($ または value メソッドのいずれかを使用して、consumer および producer パーツを提供できることに注意してください)。

共通

input または outputMessage セクションでは、基本クラスまたは静的インポートで定義した method (たとえば、assertThatMessageIsOnTheQueue()) の名前を使用して assertThat を呼び出すことができます。Spring Cloud Contract は、生成されたテストでそのメソッドを実行します。

統合

次の統合構成のいずれかを使用できます。

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.MessageVerifierSender および org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver です。メッセージの送受信メソッドを定義します。

テストでは、ContractVerifierMessageExchange を挿入して、契約に従ったメッセージを送受信できます。次に、@AutoConfigureMessageVerifier をテストに追加します。次の例は、その方法を示しています。

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

  @Autowired
  private MessageVerifier verifier;
  ...
}
テストにスタブも必要な場合は、@AutoConfigureStubRunner にメッセージング構成が含まれるため、必要なアノテーションは 1 つだけです。

プロデューサー側のメッセージングテストの生成

DSL に input または outputMessage セクションがあると、発行者側でテストが作成されます。デフォルトでは、JUnit 4 テストが作成されます。ただし、JUnit 5、TestNG、または Spock テストを作成する可能性もあります。

messageFrom または sentTo に渡される宛先は、メッセージングの実装ごとに異なる意味を持つ可能性があります。ストリームと統合の場合、最初にチャネルの 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")
	}

}

コンシューマースタブの生成

HTTP 部分とは異なり、メッセージングでは、スタブを使用して JAR 内の契約定義を公開する必要があります。次に、コンシューマー側で解析され、適切なスタブ化されたルートが作成されます。

クラスパス上に複数のフレームワークがある場合、Stub Runner はどれを使用するかを定義する必要があります。クラスパスに AMQP、Spring Cloud Stream、および Spring Integration があり、Spring AMQP を使用すると仮定します。次に、stubrunner.stream.enabled=false と stubrunner.integration.enabled=false を設定する必要があります。そうすれば、残るフレームワークは Spring AMQP だけになります。

スタブトリガー

メッセージをトリガーするには、次の例に示すように、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')

グループ ID およびアーティファクト ID によるトリガー

次の例は、グループ ID とアーティファクト ID によってメッセージをトリガーする方法を示しています。

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

アーティファクト ID によるトリガー

次の例は、アーティファクト ID からメッセージをトリガーする方法を示しています。

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

すべてのメッセージをトリガー

次の例は、すべてのメッセージをトリガーする方法を示しています。

stubFinder.trigger()

Apache Camel を使用したコンシューマー側のメッセージング

Spring Cloud Contract Stub Runner のメッセージングモジュールを使用すると、Apache Camel と簡単に統合できます。提供されたアーティファクトについては、スタブが自動的にダウンロードされ、必要なルートが登録されます。

Apache Camel をプロジェクトに追加する

Apache Camel と Spring Cloud Contract の両方のスタブランナーをクラスパス上に置くことができます。テストクラスに @AutoConfigureStubRunner のアノテーションを付けることを忘れないでください。

機能を無効にする

この機能を無効にする必要がある場合は、stubrunner.camel.enabled=false プロパティを設定します。

サンプル

camelService アプリケーションのスタブがデプロイされた次の Maven リポジトリがあると仮定します。

└── .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

さらに、スタブには次の構造が含まれていると仮定します。

├── 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 と簡単に統合できます。提供されたアーティファクトについては、スタブが自動的にダウンロードされ、必要なルートが登録されます。

プロジェクトへのランナーの追加

Spring Integration と Spring Cloud Contract の両方のスタブランナーをクラスパス上に置くことができます。テストクラスに @AutoConfigureStubRunner のアノテーションを付けることを忘れないでください。

機能を無効にする

この機能を無効にする必要がある場合は、stubrunner.integration.enabled=false プロパティを設定します。

サンプル

integrationService アプリケーションのスタブがデプロイされた次の Maven リポジトリがあると仮定します。

└── .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

さらに、スタブに次の構造が含まれていると仮定します。

├── 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 と簡単に統合できます。提供されたアーティファクトについては、スタブが自動的にダウンロードされ、必要なルートが登録されます。

スタブランナーとストリーム messageFrom または sentTo 文字列との統合が最初にチャネルの 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')

プロジェクトへのランナーの追加

Spring Cloud Stream と Spring Cloud Contract の両方のスタブランナーをクラスパスに含めることができます。テストクラスに @AutoConfigureStubRunner のアノテーションを付けることを忘れないでください。

機能を無効にする

この機能を無効にする必要がある場合は、stubrunner.stream.enabled=false プロパティを設定します。

サンプル

streamService アプリケーションのスタブがデプロイされた次の Maven リポジトリがあると仮定します。

└── .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

さらに、スタブに次の構造が含まれていると仮定します。

├── 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 ブローカーのインスタンスが実行中であることを前提としています。

プロジェクトへのランナーの追加

クラスパス上に Spring JMS と Spring Cloud Contract スタブランナーの両方が必要です。テストクラスに @AutoConfigureStubRunner のアノテーションを付けることを忘れないでください。

サンプル

スタブ構造が次のようになっていると仮定します。

├── 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')

これにより、契約の出力メッセージに記載されている宛先にメッセージが送信されます。