メッセージング
Spring Cloud Contract を使用すると、通信手段としてメッセージングを使用するアプリケーションを検証できます。このドキュメントに示されている統合はすべて Spring で動作しますが、独自の統合を作成して使用することもできます。
メッセージング DSL のトップレベル要素
メッセージング用の DSL は、HTTP に重点を置いた DSL とは少し異なります。次のセクションでは、その違いについて説明します。
メソッドによってトリガーされる出力
次の例に示すように、出力メッセージはメソッド (契約の開始時やメッセージの送信時の Scheduler
など) を呼び出すことによってトリガーできます。
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')
}
}
}
# 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
を使用してメッセージをトリガーできます。
統合
次の統合構成のいずれかを使用できます。
Apache Camel
Spring Integration
Spring JMS
Spring Boot を使用しているため、これらのライブラリのいずれかをクラスパスに追加すると、すべてのメッセージング構成が自動的にセットアップされます。
生成されたテストの基本クラスに @AutoConfigureMessageVerifier を忘れずに配置してください。そうしないと、Spring Cloud Contract のメッセージング部分が機能しません。 |
Spring Cloud Stream を使用する場合は、次のように Maven
Gradle
|
手動による統合テスト
テストで使用される主なインターフェースは 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 ) です。 |
次の契約について考えてみましょう。
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())
}
}
}
label: some_label
input:
triggeredBy: bookReturnedTriggered
outputMessage:
sentTo: activemq:output
body:
bookName: foo
headers:
BOOK-NAME: foo
contentType: application/json
前述の例では、次のテストが作成されます。
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");
}
}
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
には、メッセージをトリガーするための次のオプションがあります。
グループ ID およびアーティファクト ID によるトリガー
次の例は、グループ ID とアーティファクト ID によってメッセージをトリガーする方法を示しています。
stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')
Apache Camel を使用したコンシューマー側のメッセージング
Spring Cloud Contract Stub Runner のメッセージングモジュールを使用すると、Apache Camel と簡単に統合できます。提供されたアーティファクトについては、スタブが自動的にダウンロードされ、必要なルートが登録されます。
Apache Camel をプロジェクトに追加する
Apache Camel と Spring Cloud Contract の両方のスタブランナーをクラスパス上に置くことができます。テストクラスに @AutoConfigureStubRunner
のアノテーションを付けることを忘れないでください。
サンプル
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
のアノテーションを付けることを忘れないでください。
サンプル
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 を使用する場合は、次のように Maven
Gradle
|
プロジェクトへのランナーの追加
Spring Cloud Stream と Spring Cloud Contract の両方のスタブランナーをクラスパスに含めることができます。テストクラスに @AutoConfigureStubRunner
のアノテーションを付けることを忘れないでください。
サンプル
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')
これにより、契約の出力メッセージに記載されている宛先にメッセージが送信されます。