Fixing the way queue binding key gets used for rabbit listener

without this change we're very strict in terms of the number of called send methods on the rabbittemplate
with this change we accept that the method could have been called more than once

fixes #332
This commit is contained in:
Marcin Grzejszczak
2017-06-28 11:33:53 +02:00
parent 49a542e214
commit 36409ac400
5 changed files with 198 additions and 14 deletions

View File

@@ -666,7 +666,7 @@ package com.example.fraud;
import org.junit.Before;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
public class FraudBase {
@Before
@@ -1109,15 +1109,15 @@ Example of a `pom.xml` inside the `server` folder.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.BUILD-SNAPSHOT</version>
<version>1.5.4.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<spring-cloud-contract.version>1.1.2.BUILD-SNAPSHOT</spring-cloud-contract.version>
<spring-cloud-dependencies.version>Dalston.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
<spring-cloud-contract.version>1.2.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
<spring-cloud-dependencies.version>Edgware.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
<excludeBuildFolders>true</excludeBuildFolders>
</properties>

View File

@@ -171,10 +171,10 @@ class JUnitMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder {
request = "${request}\n\t\t\t\t\"${StringEscapeUtils.escapeJava(bodyAsString)}\"\n"
}
if (inputMessage.messageHeaders) {
request = "${request}\t\t\t\t, headers()\n"
request = "${request}\t\t\t\t, headers()"
}
inputMessage.messageHeaders?.executeForEachHeader { Header header ->
request = "${request}\t\t\t\t\t\t${getHeaderString(header)}"
request = "${request}\n\t\t\t\t\t\t${getHeaderString(header)}"
}
return "${request}\n\t\t\t)"
}

View File

@@ -16,13 +16,6 @@
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.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -40,6 +33,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.util.Assert;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.verify;
import static org.springframework.amqp.support.converter.DefaultClassMapper.DEFAULT_CLASSID_FIELD_NAME;
/**
* {@link MessageVerifier} implementation to integrate with plain spring-amqp/spring-rabbit.
* It is meant to be used without interacting with a running bus.
@@ -99,7 +100,7 @@ public class SpringAmqpStubMessages implements
@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));
verify(this.rabbitTemplate, atLeastOnce()).send(eq(destination), anyString(), messageCaptor.capture(), any(CorrelationData.class));
if (messageCaptor.getAllValues().isEmpty()) {
log.info("no messages found on destination {}", destination);

View File

@@ -0,0 +1,116 @@
package com.example;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
@Component
public class RabbitManager {
public static final Logger LOG = LoggerFactory.getLogger(RabbitManager.class);
private BookService service;
private RabbitTemplate rabbitTemplate;
@Autowired
public RabbitManager(BookService service, RabbitTemplate rabbitTemplate) {
this.service = service;
this.rabbitTemplate = rabbitTemplate;
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(),
exchange = @Exchange(
value = "input",
durable="true",
autoDelete="false",
type="topic"),
key = "event"
))
public void newBook(Book book, @Headers Map<String, String> headers) {
LOG.info("Received new book with bookname = " + book.getName());
LOG.info("Headers = " + headers);
service.sendBook(book, headers.get("amqp_replyTo"));
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(),
exchange = @Exchange(
value = "input",
durable="true",
autoDelete="false",
type="topic"),
key = "event2"
))
public void newBook2(Book book, @Headers Map<String, String> headers) {
LOG.info("newBook2 Received new book with bookname = " + book.getName());
LOG.info("newBook2 Headers = " + headers);
service.sendBook(book, headers.get("amqp_replyTo"));
}
}
interface BookService {
void sendBook(Book book, String replyTo);
void newBook(Book book);
Book getBook(int index);
int noOfBooks();
List<Book> getBooks();
}
@Component
class BookServiceImpl implements BookService {
public static final Logger LOG = LoggerFactory.getLogger(BookServiceImpl.class);
private List<Book> books;
private RabbitTemplate rabbitTemplate;
@Autowired
public BookServiceImpl(RabbitTemplate rabbitTemplate) {
books = new LinkedList<>();
this.rabbitTemplate = rabbitTemplate;
}
@Override
public void sendBook(Book book, String replyTo) {
LOG.info("Received new book with bookname = " + book.getName());
newBook(book);
rabbitTemplate.convertAndSend("", replyTo, book);
}
@Override
public void newBook(Book book) {
books.add(book);
}
@Override
public Book getBook(int index) {
return books.get(index);
}
@Override
public int noOfBooks() {
return books.size();
}
@Override
public List<Book> getBooks() {
return books;
}
}

View File

@@ -25,6 +25,7 @@ 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.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.test.context.ContextConfiguration
@@ -33,6 +34,9 @@ import spock.lang.Specification
import javax.inject.Inject
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.assertj.core.api.Assertions.assertThat
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
// Context configuration would end up in base class
@ContextConfiguration(classes = [AmqpMessagingApplication], loader = SpringBootContextLoader)
@AutoConfigureMessageVerifier
@@ -79,6 +83,69 @@ class AmqpMessagingApplicationSpec extends Specification {
JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some')
}
@Issue("332")
def "should work for second scenario"() {
given:
def dsl =
Contract.make {
description("""
Represents scenario 2 from documentation:
http://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_publisher_side_test_generation
"The input message triggers an output message."
```
given:
rabbit service is running
when:
input message is received
then:
message is send
```
""")
label 'some_label2'
input {
messageFrom('input')
messageBody([
name: 'foo2'
])
messageHeaders {
messagingContentType(applicationJson())
header('amqp_replyTo', 'amq.rabbitmq.reply-to')
header('bill', 'bill')
}
}
outputMessage {
sentTo('')
body('''{ "name" : "foo2" }''')
headers {
messagingContentType(applicationJson())
}
}
}
// generated test should look like this:
and:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\"name\":\"foo2\"}"
, headers()
.header("contentType", "application/json")
.header("amqp_replyTo", "amq.rabbitmq.reply-to")
.header("bill", "bill")
)
when:
contractVerifierMessaging.send(inputMessage, "input")
then:
ContractVerifierMessage response = contractVerifierMessaging.receive("")
assertThat(response).isNotNull()
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("['name']").isEqualTo("foo2")
}
@Issue("178")
def "should work for input/output when bytes are used"() {
given: