add spring-amqp/spring-rabbit support for message contracts (#106)

This commit is contained in:
Mathias Düsterhöft
2016-10-20 12:43:31 +02:00
committed by Marcin Grzejszczak
parent 74ea3fd27d
commit 2b70b450b9
30 changed files with 987 additions and 5 deletions

View File

@@ -305,7 +305,7 @@ with __Contract Definition Language__ (DSL). Contract definitions are used to pr
* JSON stub definitions to be used by WireMock when doing integration testing on the client code (__client tests__).
Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier.
* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to
* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream, Spring AMQP and Apache Camel. You can however set your own integrations if you want to
* Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__).
Full test is generated by Spring Cloud Contract Verifier.

View File

@@ -11,7 +11,7 @@ with __Contract Definition Language__ (DSL). Contract definitions are used to pr
* JSON stub definitions to be used by WireMock when doing integration testing on the client code (__client tests__).
Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier.
* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to
* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream, Spring AMQP and Apache Camel. You can however set your own integrations if you want to
* Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__).
Full test is generated by Spring Cloud Contract Verifier.

View File

@@ -5,11 +5,12 @@ All of our integrations are working with Spring but you can also create one your
==== Integrations
You can use one of the three integration configurations:
You can use one of the four integration configurations:
- Apache Camel
- Spring Integration
- Spring Cloud Stream
- Spring AMQP
Since we're using Spring Boot then if you have added one of the aforementioned libraries
to the classpath then automatically all the messaging configuration will be set up.

View File

@@ -5,6 +5,7 @@ Stub Runner has the functionality to run the published stubs in memory. It can i
- Spring Integration
- Spring Cloud Stream
- Apache Camel
- Spring AMQP
It also provides points of entry to integrate with any other solution on the market.
@@ -53,4 +54,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-camel/README.adoc[]
include::{tests_path}/spring-cloud-contract-stub-runner-integration/README.adoc[]
include::{tests_path}/spring-cloud-contract-stub-runner-stream/README.adoc[]
include::{tests_path}/spring-cloud-contract-stub-runner-stream/README.adoc[]
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/README.adoc[]

10
pom.xml
View File

@@ -104,6 +104,16 @@
<artifactId>spock-global-unroll</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>1.6.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-dependencies</artifactId>

View File

@@ -36,6 +36,11 @@
<artifactId>spring-cloud-stream-test-support</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
@@ -90,7 +95,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import org.mockito.Mockito;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.amqp.support.SimpleAmqpHeaderMapper;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.support.converter.MessagingMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration setting up {@link MessageVerifier} for use with plain spring-rabbit/spring-amqp
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
@Configuration
@ConditionalOnClass({Message.class, RabbitTemplate.class, Mockito.class})
@ConditionalOnMissingClass("org.springframework.integration.core.MessageSource")
@AutoConfigureBefore(ContractVerifierIntegrationConfiguration.class)
@AutoConfigureAfter(ContractVerifierStreamAutoConfiguration.class)
public class ContractVerifierAmqpAutoConfiguration {
@SpyBean
private RabbitTemplate rabbitTemplate;
@Autowired(required = false)
private MessageListenerAdapter messageListenerAdapter;
@Bean
@ConditionalOnMissingBean
public MessageVerifier<Message> contractVerifierMessageExchange() {
return new SpringAmqpStubMessages(this.rabbitTemplate, this.messageListenerAdapter);
}
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierMessaging(
MessageVerifier<Message> exchange) {
return new ContractVerifierHelper(exchange, this.rabbitTemplate.getMessageConverter());
}
}
class ContractVerifierHelper extends ContractVerifierMessaging<Message> {
private final MessageConverter messageConverter;
public ContractVerifierHelper(MessageVerifier<Message> exchange, MessageConverter messageConverter) {
super(exchange);
this.messageConverter = messageConverter;
}
@Override
protected ContractVerifierMessage convert(Message message) {
MessagingMessageConverter messageConverter = new MessagingMessageConverter(this.messageConverter, new SimpleAmqpHeaderMapper());
org.springframework.messaging.Message<?> messagingMessage = (org.springframework.messaging.Message<?>) messageConverter.fromMessage(message);
return new ContractVerifierMessage(messagingMessage.getPayload(), messagingMessage.getHeaders());
}
}

View File

@@ -0,0 +1,52 @@
package org.springframework.cloud.contract.verifier.messaging.amqp;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.concurrent.ExecutorService;
import org.mockito.Mockito;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* Spring rabbit test utility that provides a mock ConnectionFactory to avoid having to connect against a running broker.
*
* Set verifier.amqp.mockConnection=true to enable the mocked ConnectionFactory
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
@Configuration
@ConditionalOnClass({Message.class, RabbitTemplate.class, Mockito.class})
@ConditionalOnMissingClass("org.springframework.integration.core.MessageSource")
@AutoConfigureAfter(ContractVerifierAmqpAutoConfiguration.class)
@ConditionalOnProperty(value = "verifier.amqp.mockConnection", havingValue = "true", matchIfMissing = true)
public class RabbitMockConnectionFactoryAutoConfiguration {
@Bean
public ConnectionFactory connectionFactory() {
com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
Connection mockConnection = mock(Connection.class);
Channel mockChannel = mock(Channel.class);
try {
when(mockConnectionFactory.newConnection((ExecutorService) null)).thenReturn(mockConnection);
when(mockConnection.isOpen()).thenReturn(true);
when(mockConnection.createChannel()).thenReturn(mockChannel);
} catch (Exception e) {
throw new RuntimeException(e);
}
return new CachingConnectionFactory(mockConnectionFactory);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.amqp;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.verify;
import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePropertiesBuilder;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.util.Assert;
/**
* {@link MessageVerifier} implementation to integrate with plain spring-amqp/spring-rabbit.
* It is meant to be used without interacting with a running bus.
*
* It relies on the RabbitTemplate to be a spy to be able to capture send messages.
*
* Messages are not sent to the bus - but are handed over to the {@link MessageListenerAdapter} which
* allows us to test the full deserialization and listener invocation.
*
* @author Mathias Düsterhöft
* @since 1.0.2
*/
public class SpringAmqpStubMessages implements
MessageVerifier<Message> {
private static final Logger log = LoggerFactory.getLogger(SpringAmqpStubMessages.class);
private final RabbitTemplate rabbitTemplate;
private final MessageListenerAdapter messageListenerAdapter;
@Autowired
public SpringAmqpStubMessages(RabbitTemplate rabbitTemplate, MessageListenerAdapter messageListenerAdapter) {
Assert.notNull(rabbitTemplate);
Assert.isTrue(mockingDetails(rabbitTemplate).isSpy() || mockingDetails(rabbitTemplate).isMock()); //we get send messages by capturing arguments on the spy
this.rabbitTemplate = rabbitTemplate;
this.messageListenerAdapter = messageListenerAdapter;
}
@Override
public <T> void send(T payload, Map<String, Object> headers, String destination) {
Message message = org.springframework.amqp.core.MessageBuilder
.withBody(((String) payload).getBytes())
.andProperties(
MessagePropertiesBuilder.newInstance()
.setContentType((String) headers.get("contentType"))
.copyHeaders(headers).build())
.build();
if (headers.containsKey(DEFAULT_CLASSID_FIELD_NAME)) {
message.getMessageProperties().setHeader(DEFAULT_CLASSID_FIELD_NAME, headers.get(DEFAULT_CLASSID_FIELD_NAME));
}
send(message, destination);
}
@Override
public void send(Message message, String destination) {
if (this.messageListenerAdapter == null) {
throw new IllegalStateException("no MessageListenerAdapter wired - cannot send message");
}
this.messageListenerAdapter.onMessage(message);
}
@Override
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
verify(this.rabbitTemplate).send(eq(destination), anyString(), messageCaptor.capture(), any(CorrelationData.class));
if (messageCaptor.getAllValues().isEmpty()) {
log.info("no messages found on destination {}", destination);
return null;
} else if (messageCaptor.getAllValues().size() > 1) {
log.info("multiple messages found on destination {} returning last one - {}", destination);
return messageCaptor.getValue();
}
return messageCaptor.getValue();
}
@Override
public Message receive(String destination) {
return receive(destination, 5, TimeUnit.SECONDS);
}
}

View File

@@ -2,5 +2,7 @@
org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier=\
org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration,\
org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration,\
org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration

View File

@@ -0,0 +1,33 @@
package org.springframework.cloud.contract.verifier.messaging.amqp
import org.springframework.amqp.core.Message
import org.springframework.amqp.core.MessageBuilder
import org.springframework.amqp.core.MessagePropertiesBuilder
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import spock.lang.Specification
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON
/**
* @author Mathias Düsterhöft
*/
class ContractVerifierHelperSpec extends Specification {
def "should convert message"() {
given:
String payload = '''{"name":"some"}'''
Message message = MessageBuilder
.withBody(payload.bytes)
.andProperties(MessagePropertiesBuilder.newInstance()
.setHeader("my-header", "some")
.setContentType(CONTENT_TYPE_JSON)
.build()).build()
ContractVerifierHelper contractVerifierHelper = new ContractVerifierHelper(null, new Jackson2JsonMessageConverter())
when:
ContractVerifierMessage contractVerifierMessage = contractVerifierHelper.convert(message)
then:
((Map) contractVerifierMessage.payload).containsKey("name")
contractVerifierMessage.headers.containsKey("contentType")
contractVerifierMessage.headers.containsKey("my-header")
}
}

View File

@@ -0,0 +1,39 @@
package org.springframework.cloud.contract.verifier.messaging.amqp
import com.google.common.collect.ImmutableMap
import org.mockito.ArgumentCaptor
import org.springframework.amqp.core.Message
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter
import spock.lang.Specification
import static org.mockito.Mockito.mock
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON
import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME
/**
* @author Mathias Düsterhöft
*/
class SpringAmqpStubMessagesSpec extends Specification {
RabbitTemplate rabbitTemplate = mock(RabbitTemplate.class)
MessageListenerAdapter messageListenerAdapter = Mock(MessageListenerAdapter.class)
def "should send amqp message with type id"() {
given:
String payload = '''{"name":"some"}'''
ArgumentCaptor<Message> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class)
SpringAmqpStubMessages messageVerifier = new SpringAmqpStubMessages(rabbitTemplate, messageListenerAdapter)
when:
messageVerifier.send(payload,
ImmutableMap.builder()
.put(DEFAULT_CLASSID_FIELD_NAME, "org.example.Some")
.put("contentType", CONTENT_TYPE_JSON)
.build(),
"test-exchange")
then:
1 * messageListenerAdapter.onMessage({
it.getMessageProperties().getContentType() == CONTENT_TYPE_JSON &&
it.getMessageProperties().getHeaders().get(DEFAULT_CLASSID_FIELD_NAME) == "org.example.Some"
})
}
}

View File

@@ -22,12 +22,14 @@
<module>samples-messaging-spring</module>
<module>samples-messaging-stream</module>
<module>samples-messaging-integration</module>
<module>samples-messaging-amqp</module>
<module>spring-cloud-contract-stub-runner-boot-eureka</module>
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
<module>spring-cloud-contract-stub-runner-camel</module>
<module>spring-cloud-contract-stub-runner-context-path</module>
<module>spring-cloud-contract-stub-runner-integration</module>
<module>spring-cloud-contract-stub-runner-stream</module>
<module>spring-cloud-contract-stub-runner-amqp</module>
</modules>
<build>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tests</artifactId>
<version>1.0.1.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-sample-amqp</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Sample Spring Amqp</name>
<description>Spring Cloud Contract Sample Spring Amqp Rabbit</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,46 @@
package com.example;
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.fasterxml.jackson.databind.ObjectMapper;
@SpringBootApplication
public class AmqpMessagingApplication {
public static void main(String[] args) {
SpringApplication.run(AmqpMessagingApplication.class, args);
}
@Bean
public MessageConverter messageConverter(ObjectMapper objectMapper) {
final Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter();
jsonMessageConverter.setJsonObjectMapper(objectMapper);
jsonMessageConverter.setCreateMessageIds(true);
final ContentTypeDelegatingMessageConverter messageConverter = new ContentTypeDelegatingMessageConverter(jsonMessageConverter);
messageConverter.addDelegate(CONTENT_TYPE_JSON, jsonMessageConverter);
return messageConverter;
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
@Bean
public Exchange testExchange() {
return new TopicExchange("test-exchange");
}
}

View File

@@ -0,0 +1,17 @@
package com.example;
import com.fasterxml.jackson.annotation.JsonCreator;
public class Book {
private String name;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public Book(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}

View File

@@ -0,0 +1,23 @@
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;
}
public void sendMessage(Book book) {
this.rabbitTemplate.convertAndSend(this.exchange.getName(), "routingkey", book);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example
import com.jayway.jsonpath.DocumentContext
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.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.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import javax.inject.Inject
// Context configuration would end up in base class
@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader)
@DirtiesContext
@AutoConfigureMessageVerifier
public class AmqpMessagingApplicationSpec extends Specification {
// ALL CASES
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
def "should work for triggered based messaging"() {
given:
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('publishBook()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo('test-exchange')
// the body of the output message
body('''{ "name" : "some" }''')
// the headers of the output message
headers {
header('contentType', 'application/json')
header('__TypeId__', 'com.example.Book')
}
}
}
// generated test should look like this:
when:
publishBook()
then:
def response = contractVerifierMessaging.receive('test-exchange')
response.headers.get('contentType') == 'application/json'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some')
}
// BASE CLASS WOULD HAVE THIS:
@Autowired MessagePublisher messagePublisher
void publishBook() {
this.messagePublisher.sendMessage(new Book("some"))
}
}

View File

@@ -0,0 +1,84 @@
=== Stub Runner Spring AMQP
Spring Cloud Contract Verifier Stub Runner's messaging module provides an easy way to integrate with Spring AMQP's Rabbit Template.
For the provided artifacts it will automatically download the stubs and register the required
routes.
The integration tries to work standalone, that is without interaction with a running RabbitMQ message broker. It expects a `RabbitTemplate` on the application context and uses it as a spring boot test `@SpyBean`.
Thus it can use the mockito spy functionality to verify and introspect messages sent by the application.
IMPORTANT: In the current status of the implementation tries to find a `MessageListenerAdapter` on the application context to send messages into the application, because we want to avoid interacting with a running bus.
Using annotation driven listener endpoints (e.g. `@RabbitListener` annotated listener methods) is currently not supported.
==== Adding it to the project
It's enough to have both Spring AMQP and Spring Cloud Contract Stub Runner on classpath.
Remember to annotate your test class with `@AutoConfigureMessageVerifier`.
==== Examples
===== Stubs structure
Let us assume that we have the following Maven repository with a deployed stubs for the
`spring-cloud-contract-amqp-test` application.
[source,bash,indent=0]
----
└── .m2
└── repository
└── com
└── example
└── spring-cloud-contract-amqp-test
├── 0.4.0-SNAPSHOT
│   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
│   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
│   └── maven-metadata-local.xml
└── maven-metadata-local.xml
----
And the stubs contain the following structure:
[source,bash,indent=0]
----
├── META-INF
│   └── MANIFEST.MF
└── contracts
└── shouldProduceValidPersonData.groovy
----
Let's consider the following contract:
[source,groovy]
----
include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=amqp_contract,indent=0]
----
and the following Spring configuration:
[source,yaml]
----
include::src/test/resources/application.yml[]
----
===== Triggering the message
So to trigger a message using the contract above we'll use the `StubTrigger` interface as follows.
[source,groovy]
----
include::src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=client_trigger,indent=0]
----
===== Spring AMQP Test Configuration
In order to avoid that Spring AMQP is trying to connect to a running broker during our tests we configure a mock `ConnectionFactory`.
To disable the mocked ConnectionFactory set the property `verifier.amqp.mockConnection=false`
[source,yaml]
----
verifier:
amqp:
mockConnection: false
----

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tests</artifactId>
<version>1.0.1.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-stub-runner-amqp</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Stub Runner AMQP</name>
<description>Spring Cloud Contract Stub Runner AMQP</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner-jetty</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.solidsoft.spock</groupId>
<artifactId>spock-global-unroll</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,52 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp;
import static org.springframework.amqp.core.MessageProperties.CONTENT_TYPE_JSON;
import org.springframework.amqp.core.Exchange;
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.adapter.MessageListenerAdapter;
import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.fasterxml.jackson.databind.ObjectMapper;
@SpringBootApplication
public class AmqpMessagingApplication {
public static void main(String[] args) {
SpringApplication.run(AmqpMessagingApplication.class, args);
}
@Bean
public MessageConverter messageConverter(ObjectMapper objectMapper) {
final Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter();
jsonMessageConverter.setJsonObjectMapper(objectMapper);
jsonMessageConverter.setCreateMessageIds(true);
final ContentTypeDelegatingMessageConverter messageConverter = new ContentTypeDelegatingMessageConverter(jsonMessageConverter);
messageConverter.addDelegate(CONTENT_TYPE_JSON, jsonMessageConverter);
return messageConverter;
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
@Bean
public Exchange testExchange() {
return new TopicExchange("test-exchange");
}
@Bean
public MessageListenerAdapter messageListenerAdapter(MessageSubscriber messageSubscriber, MessageConverter messageConverter) {
return new MessageListenerAdapter(messageSubscriber, messageConverter);
}
}

View File

@@ -0,0 +1,11 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp;
import org.springframework.stereotype.Component;
@Component
public class MessageSubscriber {
public void handleMessage(Person person) {
}
}

View File

@@ -0,0 +1,23 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp;
public class Person {
private Integer id;
private String name;
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,67 @@
package org.springframework.cloud.contract.stubrunner.messaging.amqp
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.mock.mockito.SpyBean
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.stubrunner.StubTrigger
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import static org.mockito.BDDMockito.then
@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader)
@AutoConfigureStubRunner
class AmqpStubRunnerSpec extends Specification {
@Autowired
StubTrigger stubTrigger
@SpyBean
MessageSubscriber messageSubscriber
@Captor
ArgumentCaptor<Person> personArgumentCaptor
def "should trigger stub amqp message"() {
given:
// tag::amqp_contract[]
Contract.make {
// Human readable description
description 'Should produce valid person data'
// Label by means of which the output message can be triggered
label 'contract-test.person.created.event'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('createPerson()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'contract-test.exchange'
headers {
header('contentType': 'application/json')
header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
}
// the body of the output message
body ([
id: $(consumer(9), producer(regex("[0-9]+"))),
name: "me"
])
}
}
// end::amqp_contract[]
when:
// tag::client_trigger[]
stubTrigger.trigger("contract-test.person.created.event")
// end::client_trigger[]
then:
then(messageSubscriber).should().handleMessage(personArgumentCaptor.capture())
personArgumentCaptor.value.name != null
}
}

View File

@@ -0,0 +1,2 @@
stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.springframework.cloud.contract.verifier.stubs.amqp</groupId>
<artifactId>spring-cloud-contract-amqp-test</artifactId>
<version>0.4.0-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20161007155639</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud.contract.verifier.stubs.amqp</groupId>
<artifactId>spring-cloud-contract-amqp-test</artifactId>
<version>0.4.0-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<metadata>
<groupId>org.springframework.cloud.contract.verifier.stubs.amqp</groupId>
<artifactId>spring-cloud-contract-amqp-test</artifactId>
<version>0.4.0-SNAPSHOT</version>
<versioning>
<versions>
<version>0.4.0-SNAPSHOT</version>
</versions>
<lastUpdated>20160409062112</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<metadata>
<groupId>org.springframework.cloud.contract.verifier.stubs.amqp</groupId>
<artifactId>spring-cloud-contract-amqp-test</artifactId>
<version>0.4.0-SNAPSHOT</version>
<versioning>
<versions>
<version>0.4.0-SNAPSHOT</version>
</versions>
<lastUpdated>20160409062112</lastUpdated>
</versioning>
</metadata>