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

@@ -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"
})
}
}