spring-cloud-contract icon indicating copy to clipboard operation
spring-cloud-contract copied to clipboard

Multiple rabbitTemplate support in Amqp contract

Open henryyonathan90 opened this issue 7 years ago • 7 comments

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?

henryyonathan90 avatar Sep 20 '18 11:09 henryyonathan90

Why would you want to support multiple rabbit templates to test a single communication? Can you elaborate more on it?

marcingrzejszczak avatar Sep 20 '18 11:09 marcingrzejszczak

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.

henryyonathan90 avatar Sep 21 '18 07:09 henryyonathan90

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.

marcingrzejszczak avatar Sep 21 '18 07:09 marcingrzejszczak

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.

henryyonathan90 avatar Sep 24 '18 03:09 henryyonathan90

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.

SebestyenMiklos avatar Jun 17 '19 09:06 SebestyenMiklos

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

SebestyenMiklos avatar Jun 18 '19 12:06 SebestyenMiklos

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.

rvervaek avatar May 27 '20 13:05 rvervaek

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.

marcingrzejszczak avatar Nov 16 '22 17:11 marcingrzejszczak