@@ -22,6 +22,11 @@ http://wiremock.org[WireMock] with different servers by using the
|
||||
https://github.com/spring-cloud/spring-cloud-contract/tree/master/samples[samples]
|
||||
for more details.
|
||||
|
||||
IMPORTANT: The Spring Cloud Release Train BOM imports `spring-cloud-contract-dependencies`
|
||||
which in turn has exclusions for the dependencies needed by WireMock. This might lead to a situation that
|
||||
even if you're not using Spring Cloud Contract then your dependencies will be influenced
|
||||
anyways.
|
||||
|
||||
If you have a Spring Boot application that uses Tomcat as an embedded
|
||||
server, for example (the default with `spring-boot-starter-web`), then
|
||||
you can simply add `spring-cloud-contract-wiremock` to your classpath
|
||||
|
||||
@@ -20,15 +20,18 @@ import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
|
||||
import groovy.json.JsonSlurper
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.stubrunner.StubRunning
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
// tag::boot_usage[]
|
||||
@ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader)
|
||||
@SpringBootTest(properties = "spring.cloud.zookeeper.enabled=false")
|
||||
@ActiveProfiles("test")
|
||||
class StubRunnerBootSpec extends Specification {
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.junit.BeforeClass
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.stubrunner.StubFinder
|
||||
import org.springframework.cloud.contract.stubrunner.StubNotFoundException
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -36,6 +37,7 @@ import spock.lang.Specification
|
||||
// Not necessary if Spring Cloud is used. TODO: make it work without this.
|
||||
// tag::test[]
|
||||
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
|
||||
@SpringBootTest(properties = [" stubrunner.cloud.enabled=false", "stubrunner.camel.enabled=false"])
|
||||
@AutoConfigureStubRunner
|
||||
@DirtiesContext
|
||||
@ActiveProfiles("test")
|
||||
|
||||
@@ -71,7 +71,6 @@ public class ContractVerifierAmqpAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MessageVerifier<Message> contractVerifierMessageExchange() {
|
||||
|
||||
return new SpringAmqpStubMessages(this.rabbitTemplate,
|
||||
new MessageListenerAccessor(this.rabbitListenerEndpointRegistry, this.simpleMessageListenerContainers, this.bindings));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
|
||||
/**
|
||||
* Abstraction hiding details of the different sources of message listeners
|
||||
*
|
||||
* Needed because {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor} adds the listners to the
|
||||
* Needed because {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor} adds the listeners to the
|
||||
* {@link RabbitListenerEndpointRegistry} so that the registry is empty when wired into an auto configuration class
|
||||
* so we wrap it in the accessor to access the listeners late at runtime
|
||||
*
|
||||
@@ -38,7 +38,6 @@ class MessageListenerAccessor {
|
||||
List<SimpleMessageListenerContainer> listenerContainers = collectListenerContainers();
|
||||
//we interpret the destination as exchange name and collect all the queues bound to this exchange
|
||||
Set<String> queueNames = collectQueuesBoundToDestination(destination);
|
||||
|
||||
return getListenersByBoundQueues(listenerContainers, queueNames);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ public class ContractVerifierObjectMapper {
|
||||
public String writeValueAsString(Object payload) throws JsonProcessingException {
|
||||
if (payload instanceof String) {
|
||||
return payload.toString();
|
||||
} else if (payload instanceof byte[]) {
|
||||
return new String((byte[]) payload);
|
||||
}
|
||||
return this.objectMapper.writeValueAsString(payload);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.cloud.contract.verifier.messaging.internal
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
class ContractVerifierObjectMapperSpec extends Specification {
|
||||
|
||||
ContractVerifierObjectMapper mapper = new ContractVerifierObjectMapper()
|
||||
|
||||
def "should convert an object into a json representation"() {
|
||||
given:
|
||||
MyClass input = new MyClass(foo: "bar")
|
||||
when:
|
||||
String result = mapper.writeValueAsString(input)
|
||||
then:
|
||||
result == '''{"foo":"bar"}'''
|
||||
}
|
||||
|
||||
def "should convert bytes into a json representation"() {
|
||||
given:
|
||||
String input = '''{"foo":"bar"}'''
|
||||
when:
|
||||
String result = mapper.writeValueAsString(input.bytes)
|
||||
then:
|
||||
result == '''{"foo":"bar"}'''
|
||||
}
|
||||
|
||||
class MyClass {
|
||||
String foo
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,10 @@ public class AmqpMessagingApplication {
|
||||
public Exchange testExchange() {
|
||||
return new TopicExchange("test-exchange");
|
||||
}
|
||||
|
||||
@Bean
|
||||
MessagePublisher messagePublisher(RabbitTemplate rabbitTemplate) {
|
||||
return new MessagePublisher(rabbitTemplate, testExchange());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.example;
|
||||
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageBuilder;
|
||||
import org.springframework.amqp.core.MessageListener;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.TopicExchange;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration
|
||||
class Issue178ListenerConfiguration {
|
||||
@Bean
|
||||
SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory, RabbitTemplate rabbitTemplate) {
|
||||
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
container.addQueueNames("rated-item-service.rated-item-event.exchange");
|
||||
container.setMessageListener(exampleListener(rabbitTemplate));
|
||||
return container;
|
||||
}
|
||||
|
||||
@Bean
|
||||
MessageListener exampleListener(final RabbitTemplate rabbitTemplate) {
|
||||
return new MessageListener() {
|
||||
public void onMessage(Message message) {
|
||||
System.out.println("received: " + message);
|
||||
try {
|
||||
String payload = new ObjectMapper().writeValueAsString(new MyPojo("992e46d8-ab05-4a26-a740-6ef7b0daeab3", "CREATED"));
|
||||
Message outputMessage = MessageBuilder.withBody(payload.getBytes()).build();
|
||||
rabbitTemplate.send(issue178OutputExchange().getName(), "routingkey", outputMessage);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static class MyPojo {
|
||||
public String ratedItemId;
|
||||
public String eventType;
|
||||
|
||||
public MyPojo(String ratedItemId, String eventType) {
|
||||
this.ratedItemId = ratedItemId;
|
||||
this.eventType = eventType;
|
||||
}
|
||||
|
||||
public MyPojo() {
|
||||
}
|
||||
}
|
||||
|
||||
@Bean Queue issue178InputQueue() {
|
||||
return new Queue("rated-item-service.rated-item-event.exchange", false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
TopicExchange issue178InputExchange() {
|
||||
return new TopicExchange("rated-item-service.rated-item-event.exchange");
|
||||
}
|
||||
|
||||
@Bean
|
||||
TopicExchange issue178OutputExchange() {
|
||||
return new TopicExchange("bill-service.rated-item-event.retry-exchange");
|
||||
}
|
||||
|
||||
@Bean Binding binding() {
|
||||
return BindingBuilder.bind(issue178InputQueue()).to(issue178InputExchange()).with("rated-item-service.rated-item-event.exchange");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,16 +2,13 @@ package com.example;
|
||||
|
||||
import org.springframework.amqp.core.Exchange;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MessagePublisher {
|
||||
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
|
||||
private final Exchange exchange;
|
||||
@Autowired
|
||||
|
||||
public MessagePublisher(RabbitTemplate rabbitTemplate, Exchange exchange) {
|
||||
this.rabbitTemplate = rabbitTemplate;
|
||||
this.exchange = exchange;
|
||||
|
||||
@@ -19,19 +19,24 @@ package com.example
|
||||
import com.jayway.jsonpath.DocumentContext
|
||||
import com.jayway.jsonpath.JsonPath
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion
|
||||
import groovy.json.JsonOutput
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import spock.lang.Issue
|
||||
import spock.lang.Specification
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
// Context configuration would end up in base class
|
||||
@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader)
|
||||
@AutoConfigureMessageVerifier
|
||||
@SpringBootTest(properties = "stubrunner.amqp.enabled=true")
|
||||
class AmqpMessagingApplicationSpec extends Specification {
|
||||
|
||||
// ALL CASES
|
||||
@@ -74,6 +79,44 @@ class AmqpMessagingApplicationSpec extends Specification {
|
||||
JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some')
|
||||
}
|
||||
|
||||
@Issue("178")
|
||||
def "should work for input/output when bytes are used"() {
|
||||
given:
|
||||
def inputBody = [
|
||||
ratedItemId: "992e46d8-ab05-4a26-a740-6ef7b0daeab3",
|
||||
eventType: "CREATED"
|
||||
]
|
||||
def dsl = Contract.make {
|
||||
label 'ratedItem-no-metricid'
|
||||
input {
|
||||
messageFrom("rated-item-service.rated-item-event.exchange")
|
||||
messageHeaders {
|
||||
header("X-tenant", "1234")
|
||||
header("contentType", "application/json")
|
||||
}
|
||||
messageBody(inputBody)
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('bill-service.rated-item-event.retry-exchange')
|
||||
body(
|
||||
ratedItemId: "992e46d8-ab05-4a26-a740-6ef7b0daeab3",
|
||||
eventType: "CREATED"
|
||||
)
|
||||
}
|
||||
}
|
||||
when:
|
||||
contractVerifierMessaging.send(contractVerifierMessaging.create(new JsonOutput().toJson(inputBody), [
|
||||
"X-tenant": "1234",
|
||||
"contentType": "application/json"
|
||||
]), "rated-item-service.rated-item-event.exchange")
|
||||
then:
|
||||
def response = contractVerifierMessaging.receive('bill-service.rated-item-event.retry-exchange')
|
||||
and:
|
||||
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
|
||||
JsonAssertion.assertThat(parsedJson).field('ratedItemId').isEqualTo('992e46d8-ab05-4a26-a740-6ef7b0daeab3')
|
||||
JsonAssertion.assertThat(parsedJson).field('eventType').isEqualTo('CREATED')
|
||||
}
|
||||
|
||||
// BASE CLASS WOULD HAVE THIS:
|
||||
|
||||
@Autowired MessagePublisher messagePublisher
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jayway.jsonpath.JsonPath
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
|
||||
import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier
|
||||
@@ -37,6 +38,7 @@ import javax.inject.Inject
|
||||
// Context configuration would end up in base class
|
||||
@ContextConfiguration(classes = [StreamMessagingApplication], loader = SpringBootContextLoader)
|
||||
@DirtiesContext
|
||||
@SpringBootTest(properties = "debug=true")
|
||||
@AutoConfigureMessageVerifier
|
||||
public class StreamMessagingApplicationSpec extends Specification {
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.stubrunner.StubFinder
|
||||
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
|
||||
@@ -40,6 +41,7 @@ import spock.lang.Specification
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
|
||||
@SpringBootTest(properties = "debug=true")
|
||||
@AutoConfigureStubRunner
|
||||
@IgnoreIf({ os.windows })
|
||||
class CamelStubRunnerSpec extends Specification {
|
||||
|
||||
@@ -21,6 +21,7 @@ import groovy.json.JsonSlurper
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.stubrunner.StubFinder
|
||||
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
|
||||
@@ -36,10 +37,12 @@ import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
|
||||
@SpringBootTest(properties = "debug=true")
|
||||
@AutoConfigureStubRunner
|
||||
@AutoConfigureMessageVerifier
|
||||
@IgnoreIf({ os.windows })
|
||||
|
||||
Reference in New Issue
Block a user