Messaging
Spring Cloud Contract lets you verify applications that use messaging as a means of communication. All of the integrations shown in this document work with Spring, but you can also create one of your own and use that.
Messaging DSL Top-level Elements
The DSL for messaging looks a little bit different than the one that focuses on HTTP. The following sections explain the differences:
Output Triggered by a Method
The output message can be triggered by calling a method (such as a Scheduler
when a contract was
started and when a message was sent), as shown in the following example:
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
In the previous example case, the output message is sent to output
if a method called
bookReturnedTriggered
is invoked. On the message publisher’s side, we generate a
test that calls that method to trigger the message. On the consumer side, you can use
some_label
to trigger the message.
Consumer/Producer
This section is valid only for the Groovy DSL. |
In HTTP, you have a notion of client
/stub and `server
/test
notation. You can also
use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also
provides the consumer
and producer
methods
(note that you can use either $
or value
methods to provide consumer
and producer
parts).
Integrations
You can use one of the following integration configurations:
-
Apache Camel
-
Spring Integration
-
Spring Cloud Stream
-
Spring JMS
Since we use Spring Boot, if you have added one of these libraries to the classpath, all the messaging configuration is automatically set up.
Remember to put @AutoConfigureMessageVerifier on the base class of your
generated tests. Otherwise, the messaging part of Spring Cloud Contract does not
work.
|
If you want to use Spring Cloud Stream, remember to add a test dependency on
Maven
Gradle
|
Manual Integration Testing
The main interface used by the tests is
org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender
and org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver
.
It defines how to send and receive messages.
In a test, you can inject a ContractVerifierMessageExchange
to send and receive
messages that follow the contract. Then add @AutoConfigureMessageVerifier
to your test.
The following example shows how to do so:
@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {
@Autowired
private MessageVerifier verifier;
...
}
If your tests require stubs as well, then @AutoConfigureStubRunner includes the
messaging configuration, so you only need the one annotation.
|
Producer Side Messaging Test Generation
Having the input
or outputMessage
sections in your DSL results in creation of tests
on the publisher’s side. By default, JUnit 4 tests are created. However, there is also a
possibility to create JUnit 5, TestNG, or Spock tests.
The destination passed to messageFrom or sentTo can have different
meanings for different messaging implementations. For Stream and Integration, it is
first resolved as a destination of a channel. Then, if there is no such destination ,
it is resolved as a channel name. For Camel, that’s a certain component (for example,
jms ).
|
Consider the following contract:
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
For the preceding example, the following test would be created:
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")
}
}
Consumer Stub Generation
Unlike in the HTTP part, in messaging, we need to publish the contract definition inside the JAR with a stub. Then it is parsed on the consumer side, and proper stubbed routes are created.
If you have multiple frameworks on the classpath, Stub Runner needs to
define which one should be used. Assume that you have AMQP, Spring Cloud Stream, and Spring Integration
on the classpath and that you want to use Spring AMQP. Then you need to set
stubrunner.stream.enabled=false and stubrunner.integration.enabled=false .
That way, the only remaining framework is Spring AMQP.
|
Stub triggering
To trigger a message, use the StubTrigger
interface, as the following example shows:
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();
}
For convenience, the StubFinder
interface extends StubTrigger
, so you need only one
or the other in your tests.
StubTrigger
gives you the following options to trigger a message:
Trigger by Label
The following example shows how to trigger a message with a label:
stubFinder.trigger('return_book_1')
Trigger by Group and Artifact IDs
The following example shows how to trigger a message by group and artifact IDs:
stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')
Consumer Side Messaging With Apache Camel
Spring Cloud Contract Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. For the provided artifacts, it automatically downloads the stubs and registers the required routes.
Adding Apache Camel to the Project
You can have both Apache Camel and Spring Cloud Contract Stub Runner on the classpath.
Remember to annotate your test class with @AutoConfigureStubRunner
.
Disabling the Functionality
If you need to disable this functionality, set the stubrunner.camel.enabled=false
property.
Examples
Assume that we have the following Maven repository with deployed stubs for the
camelService
application:
└── .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
Further, assume that the stubs contain the following structure:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ └── bookReturned1.groovy
└── mappings
Now consider the following contract:
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('rabbitmq:output?queue=output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
To trigger a message from the return_book_1
label, we use the StubTrigger
interface, as follows:
stubFinder.trigger("return_book_1")
That will send out a message to the destination described in the output message of the contract.
Consumer Side Messaging with Spring Integration
Spring Cloud Contract Stub Runner’s messaging module gives you an easy way to integrate with Spring Integration. For the provided artifacts, it automatically downloads the stubs and registers the required routes.
Adding the Runner to the Project
You can have both Spring Integration and Spring Cloud Contract Stub Runner on the
classpath. Remember to annotate your test class with @AutoConfigureStubRunner
.
Disabling the Functionality
If you need to disable this functionality, set the
stubrunner.integration.enabled=false
property.
Examples
Assume that you have the following Maven repository with deployed stubs for the
integrationService
application:
└── .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
Further assume the stubs contain the following structure:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ └── bookReturned1.groovy
└── mappings
Consider the following contract:
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
Now consider the following Spring Integration Route:
<?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>
To trigger a message from the return_book_1
label, use the StubTrigger
interface, as
follows:
stubFinder.trigger('return_book_1')
That will send out a message to the destination described in the output message of the contract.
Consumer Side Messaging With Spring Cloud Stream
Spring Cloud Contract Stub Runner’s messaging module gives you an easy way to integrate with Spring Stream. For the provided artifacts, it automatically downloads the stubs and registers the required routes.
If Stub Runner’s integration with the Stream messageFrom or sentTo strings
are resolved first as the destination of a channel and no such destination exists, the
destination is resolved as a channel name.
|
If you want to use Spring Cloud Stream, remember to add a dependency on
Maven
Gradle
|
Adding the Runner to the Project
You can have both Spring Cloud Stream and Spring Cloud Contract Stub Runner on the
classpath. Remember to annotate your test class with @AutoConfigureStubRunner
.
Disabling the Functionality
If you need to disable this functionality, set the stubrunner.stream.enabled=false
property.
Examples
Assume that you have the following Maven repository with deployed stubs for the
streamService
application:
└── .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
Further assume the stubs contain the following structure:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ └── bookReturned1.groovy
└── mappings
Consider the following contract:
Contract.make {
label 'return_book_1'
input { triggeredBy('bookReturnedTriggered()') }
outputMessage {
sentTo('returnBook')
body('''{ "bookName" : "foo" }''')
headers { header('BOOK-NAME', 'foo') }
}
}
Now consider the following Spring Cloud Stream function configuration:
@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
@Configuration(proxyBeanMethods = true)
@EnableAutoConfiguration
protected static class Config {
@Bean
Function<String, String> test1() {
return (input) -> {
println "Test 1 [${input}]"
return input
}
}
}
Now consider the following Spring configuration:
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
To trigger a message from the return_book_1
label, use the StubTrigger
interface as
follows:
stubFinder.trigger('return_book_1')
That will send out a message to the destination described in the output message of the contract.
Consumer Side Messaging With Spring JMS
Spring Cloud Contract Stub Runner’s messaging module provides an easy way to integrate with Spring JMS.
The integration assumes that you have a running instance of a JMS broker.
Adding the Runner to the Project
You need to have both Spring JMS and Spring Cloud Contract Stub Runner on the classpath. Remember to annotate your test class
with @AutoConfigureStubRunner
.
Examples
Assume that the stub structure looks as follows:
├── stubs
└── bookReturned1.groovy
Further assume the following test configuration:
stubrunner:
repository-root: stubs:classpath:/stubs/
ids: my:stubs
stubs-mode: remote
spring:
activemq:
send-timeout: 1000
jms:
template:
receive-timeout: 1000
Now consider the following contract:
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOKNAME', 'foo')
}
}
}
To trigger a message from the return_book_1
label, we use the StubTrigger
interface, as follows:
stubFinder.trigger('return_book_1')
That will send out a message to the destination described in the output message of the contract.