简体   繁体   中英

How to wait for a spring jms listener thread to finish executing in Junit test

I have a spring boot application that uses spring-JMS. Is there any way to tell the test method to wait the jms lister util it finishes executing without using latches in the actual code that will be tested?

Here is the JMS listener code:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.Message;
import javax.jms.QueueSession;


@Component
public class MyListener {


    @Autowired
    MyProcessor myProcessor;


    @JmsListener(destination = "myQueue", concurrency = "1-4")
    private void onMessage(Message message, QueueSession session) {
        myProcessor.processMessage(message, session);
    }
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.jms.Message;
import javax.jms.QueueSession;

@Component
public class MyProcessor {


    public void processMessage(Message msg, QueueSession session) {
     //Here I have some code. 

    }

}

import org.apache.activemq.command.ActiveMQTextMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.QueueSession;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
public class IntegrationTest {



    @Autowired
    private JmsTemplate JmsTemplate;

    @Test
    public void myTest() throws JMSException {
        Message message = new ActiveMQTextMessage();

        jmsTemplate.send("myQueue", session -> message);

        /*
          Here I have some testing code. How can I tell the application 
          to not execute this testing code until all JMS lister threads 
          finish executing. 
        */


    }
}

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.util.SocketUtils;

import javax.jms.ConnectionFactory;

@EnableJms
@Configuration
@Profile("test")
public class JmsTestConfig {

    public static final String BROKER_URL =
            "tcp://localhost:" + SocketUtils.findAvailableTcpPort();


    @Bean
    public BrokerService brokerService() throws Exception {
        BrokerService brokerService = new BrokerService();
        brokerService.setPersistent(false);
        brokerService.addConnector(BROKER_URL);

        return brokerService;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory(BROKER_URL);
    }

    @Bean
    public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
        JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
        return jmsTemplate;
    }

}

Note: Is it applicable to solve this without adding testing purpose code to the implementation code (MyListener and MyProcessor).

Proxy the listener and add an advice to count down a latch; here's one I did for a KafkaListener recently...

@Test
public void test() throws Exception {
    this.template.send("so50214261", "foo");
    assertThat(TestConfig.latch.await(10, TimeUnit.SECONDS)).isTrue();
    assertThat(TestConfig.received.get()).isEqualTo("foo");
}

@Configuration
public static class TestConfig {

    private static final AtomicReference<String> received = new AtomicReference<>();

    private static final CountDownLatch latch = new CountDownLatch(1);

    @Bean
    public static MethodInterceptor interceptor() {
        return invocation -> {
            received.set((String) invocation.getArguments()[0]);
            return invocation.proceed();
        };
    }

    @Bean
    public static BeanPostProcessor listenerAdvisor() {
        return new ListenerWrapper(interceptor());
    }

}

public static class ListenerWrapper implements BeanPostProcessor, Ordered {

    private final MethodInterceptor interceptor;

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    public ListenerWrapper(MethodInterceptor interceptor) {
        this.interceptor = interceptor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof Listener) {
            ProxyFactory pf = new ProxyFactory(bean);
            NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(this.interceptor);
            advisor.addMethodName("listen");
            pf.addAdvisor(advisor);
            return pf.getProxy();
        }
        return bean;
    }

}

(but you should move the countDown to after the invocation proceed() ).

A method annotated with @JmsListener deletes the message after it finishes, so a good option is to read the queue for existing messages and assume the queue is empty after your method is done. Here is the piece of code for counting the messages from the queue.

private int countMessages() {
    return jmsTemplate.browse(queueName, new BrowserCallback<Integer>() {
      @Override
      public Integer doInJms(Session session, QueueBrowser browser) throws JMSException {
        return Collections.list(browser.getEnumeration()).size();
      }
    });
  }

Following is the code for testing the countMessages() method.

jmsTemplate.convertAndSend(queueName, "***MESSAGE CONTENT***");
while (countMessages() > 0) {
    log.info("number of pending messages: " + countMessages());
    Thread.sleep(1_000l);
}
// continue with your logic here

I've based my solution on the answer given by Gary Russell, but rather put the CountDownLatch in an Aspect, using Spring AOP (or the spring-boot-starter-aop variant).

public class TestJMSConfiguration {

private static final Logger LOGGER = LoggerFactory.getLogger(TestJMSConfiguration.class);

public static final CountDownLatch countDownLatch = new CountDownLatch(1);

@Component
@Aspect
public static class LatchCounterAspect {

    @Pointcut("execution(public void be.infrabel.rocstdm.application.ROCSTDMMessageListener.onMessage(javax.jms.TextMessage))")
    public void onMessageMethod() {};

    @After(value = "onMessageMethod()")
    public void countDownLatch() {
        countDownLatch.countDown();
        LOGGER.info("CountDownLatch called. Count now at: {}", countDownLatch.getCount());
    }

}

A snippet of the test:

    JmsTemplate jmsTemplate = new JmsTemplate(this.embeddedBrokerConnectionFactory);
    jmsTemplate.convertAndSend("AQ.SOMEQUEUE.R", message);

    TestJMSConfiguration.countDownLatch.await();

    verify(this.listenerSpy).putResponseOnTargetQueueAlias(messageCaptor.capture()); 
RouteMessage outputMessage = messageCaptor.getValue();

The listenerSpy is a @SpyBean annotated field of the type of my MessageListener. The messageCaptor is a field of type ArgumentCaptor<MyMessageType> annotated with @Captor. Both of these are coming from mockito so you need to run/extend your test with both MockitoExtension (or -Runner) along with the SpringExtension (or -Runner).

My code puts an object on an outbound queue after processing the incoming message, hence the putResponseOnTargetQueueAlias method. The captor is to intercept that object and do my assertions accordingly. The same strategy could be applied to capture some other object in your logic.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM