Multiple rabbitTemplate support in Amqp contract
Hi Teams,
I am trying to implement spring cloud contract for amqp testing and just can't find any clean solution how to handle multiple rabbitTemplate.
Reading ContractVerifierAmqpAutoConfiguration.class it does seems like it only support one rabbitTemplate.
Currently I handled this by writing custom class for that:
public class CustomSpringAmqpStubMessages implements MessageVerifier<Message> {
private static final Logger log = LoggerFactory.getLogger(SpringAmqpStubMessages.class);
private final Map<String, RabbitTemplate> rabbitTemplateMap;
private final CustomMessageListenerAccessor customMessageListenerAccessor;
private RabbitTemplate activeRabbitTemplate;
@Autowired
public CustomSpringAmqpStubMessages(Map<String, RabbitTemplate> rabbitTemplateMap,
CustomMessageListenerAccessor customMessageListenerAccessor) {
// Assert.notNull(rabbitTemplates);
// Assert.isTrue(mockingDetails(rabbitTemplates).isSpy()
// || mockingDetails(rabbitTemplates).isMock()); //we get send messages by capturing arguments on the spy
this.rabbitTemplateMap = rabbitTemplateMap;
this.customMessageListenerAccessor = customMessageListenerAccessor;
}
@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 != null && headers.containsKey(DEFAULT_CLASSID_FIELD_NAME)) {
message.getMessageProperties()
.setHeader(DEFAULT_CLASSID_FIELD_NAME, headers.get(DEFAULT_CLASSID_FIELD_NAME));
}
if (headers != null && headers.containsKey(AmqpHeaders.RECEIVED_ROUTING_KEY)) {
message.getMessageProperties()
.setReceivedRoutingKey((String) headers.get(AmqpHeaders.RECEIVED_ROUTING_KEY));
}
send(message, destination);
}
@Override
public void send(Message message, String destination) {
final String routingKey = message.getMessageProperties().getReceivedRoutingKey();
List<SimpleMessageListenerContainer> listenerContainers =
this.customMessageListenerAccessor.getListenerContainersForDestination(destination,
routingKey);
if (listenerContainers.isEmpty()) {
throw new IllegalStateException("no listeners found for destination " + destination);
}
for (SimpleMessageListenerContainer listenerContainer : listenerContainers) {
MessageListener messageListener = (MessageListener) listenerContainer.getMessageListener();
messageListener.onMessage(message);
}
}
@Override
public Message receive(String destination, long timeout, TimeUnit timeUnit) {
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
ArgumentCaptor<String> routingKeyCaptor = ArgumentCaptor.forClass(String.class);
verify(this.activeRabbitTemplate, atLeastOnce()).send(eq(destination),
routingKeyCaptor.capture(),
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();
}
Message message = messageCaptor.getValue();
if (!routingKeyCaptor.getValue().isEmpty()) {
log.info("routing key passed {}", routingKeyCaptor.getValue());
message.getMessageProperties().setReceivedRoutingKey(routingKeyCaptor.getValue());
}
return message;
}
@Override
public Message receive(String destination) {
return receive(destination, 5, TimeUnit.SECONDS);
}
public void setActiveRabbitTemplate(String beanName) {
activeRabbitTemplate = rabbitTemplateMap.get(beanName);
Assert.notNull(activeRabbitTemplate);
Assert.isTrue(mockingDetails(activeRabbitTemplate).isSpy()
|| mockingDetails(activeRabbitTemplate).isMock()); //we get send messages by capturing arguments on the spy
}
}
And the configure it using:
@SpyBean(name = "integratorBookRabbitTemplate")
private RabbitTemplate integratorBookRabbitTemplate;
@SpyBean(name = "cartBookingRabbitTemplate")
private RabbitTemplate cartBookingRabbitTemplate;
@SpyBean(name = "integratorConfirmBookRabbitTemplate")
private RabbitTemplate integratorConfirmBookRabbitTemplate;
@Autowired
private Map<String,RabbitTemplate> rabbitTemplateMap;
@Bean
public CustomSpringAmqpStubMessages customSpringAmqpStubMessages() {
return new CustomSpringAmqpStubMessages(rabbitTemplateMap,
new CustomMessageListenerAccessor(this.rabbitListenerEndpointRegistry,
this.simpleMessageListenerContainers,
this.bindings));
}
As for the usage, I prepare one method to be triggered by the contract:
protected void bookRequestEvent() {
customSpringAmqpStubMessages.setActiveRabbitTemplate("integratorBookRabbitTemplate");
customContractVerifierHelper.setMessageConverter(integratorBookRabbitTemplate.getMessageConverter());
MockMvcRequestSpecification request = given().header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("storeId", "10001")
.header("requestId", "RANDOM")
.header("channelId", "web")
.header("businessChannel", "web")
.header("clientId", "cart-revo")
.header("username", "cart-revo")
.body("some json body");
// when:
given().spec(request).post("/api/book");
}
Well, in the end I still have to manually specify which rabbitTemplate and converter to be used during the test. Is there any other simpler way I am missing to implement this?
Why would you want to support multiple rabbit templates to test a single communication? Can you elaborate more on it?
Hi @marcingrzejszczak ,
so the case is my service is connecting to two different rabbit server, each serving different purpose. Therefore at the very least I need to create two different connection factory and two rabbit template.
Additionally I don't want to specify exchange and routing key each time sending a message using the rabbit templates, so I created several of them with pre-configured exchange and routing key, making sure other dev's can just use it.
so the case is my service is connecting to two different rabbit server, each serving different purpose. Therefore at the very least I need to create two different connection factory and two rabbit template.
You can create 2 separate contracts with 2 separate base classes for those tests, where you would have 2 separate configurations, where you would setup two different rabbit templates.
Hmm, well creating 2 separate base classes do seems solving the case, but in the end I still have some problem regarding the ContractVerifierAmqpAutoConfiguration.
@SpyBean
private RabbitTemplate rabbitTemplate;
This lines from ContractVerifierAmqpAutoConfiguration will always be null value since I have multiple rabbit templates and none of them defined with bean name "rabbitTemplate". Then I checked these two beans definition and thinking to manually create them while specifying rabbit template to use.
@Bean
@ConditionalOnMissingBean
public MessageVerifier<Message> contractVerifierMessageExchange() {
return new SpringAmqpStubMessages(this.rabbitTemplate,
new MessageListenerAccessor(this.rabbitListenerEndpointRegistry, this.simpleMessageListenerContainers, this.bindings));
}
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierMessaging(
MessageVerifier<Message> exchange) {
return new ContractVerifierHelper(exchange, this.rabbitTemplate.getMessageConverter());
}
Well, its not directly possible since MessageListenerAccessor and ContractVerifierHelper is not public.
In the end I solve this by creating custom SpringAmqpStubMessages, MessageListenerAccessor, ContractVerifierHelper, copying the original implementation and adding utility to set rabbit template.
Can I suggest to make MessageListenerAccessor and ContractVerifierHelper public? And make it extendable would be nice too.
Hi @spencergibb an @marcingrzejszczak, could you share the feedback as I am also facing the issue, where rabbitamplate is always null in ContractVerifierAmqpAutoConfiguration class, however I do have RabbitTemplate on classpath. Using older version of Stub Runner (1.2.6.RELEASE), as working with an older spring-boot version.
Sorry, the problem was the lack of knowledge about SpyBean usage, There were multiple RabbitTemplate Beans and none of them were 'Primary'/'qualified' , as SpyBean documentation states: "If there is more than one bean of the requested type, qualifier metadata must be specified at field level".
Reference: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/SpyBean.html
I'm too are having issues that the ContractVerifierAmqpAutoConfiguration class is relying on a single RabbitTemplate bean. Simply specifying the qualifier for the bean name can resolve this situation as stated in the documentation of SpyBean.
We've decided to go with the middleware based approach for messaging as presented in https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/main/producer_rabbit_middleware and https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/main/consumer_rabbit_middleware .
That means we want to encourage the user to use e.g. testcontainers to start an actual broker and then use the MessageVerifierSender / MessageVerifierReceiver implementations to tell us how to send and receive a message. In those beans you can define whatever rabbitTemplate you want.