How to wait for @JMSListener annotated method to c

2019-08-07 04:59发布

问题:

So I am trying to get some integration testing of JMS processing, Spring (v4.1.6) based code.

It's a very standard Spring setup with @JmsListener annotated method and a DefaultMessageListenerContainer with concurrency set to 1 and therefore allowing only 1 listening thread.

Now, I leveraged ActiveMQ's embedded broker not to rely on any external jms broker for the tests to run anywhere anytime (I should work in marketing).

So it all wires fine and then I have my JUnit test:

@Test
public void test() {
    sendSomeMessage();
    //how to wait here for the @JMSListener method to complete
    verify();
}

I send the message, but then I need to somehow wait for the @JMSListener annotated method to complete. How do I do this?

回答1:

Well, I was hoping I can somehow hook into the Message Driven Pojos lifecycle to do this, but going through other SO questions about async code I came up with a solution based on CountDownLatch

  1. The @JMSListener annotaded method should call countDown() on a CountDownLatch after all the work is complete:

    @JmsListener(destination = "dest", containerFactory = "cf")
    public void processMessage(TextMessage message) throws JMSException {
        //do the actual processing
        actualProcessing(message);
        //if there's countDownLatch call the countdown.
        if(countDownLatch != null) {
            countDownLatch.countDown();
        }
    }
    
  2. In the testMethod

    @Test
    public void test() throws InterruptedException {
        //initialize the countDownLatch and set in on the processing class
        CountDownLatch countDownLatch = new CountDownLatch(1);
        messageProcessor.setCountDownLatch(countDownLatch);
        //sendthemessage
        sendSomeMessage();
        //wait for the processing method to call countdown()
        countDownLatch.await();
        verify();
    }
    

The drawback of this solution is that you have to actually change your @JMSListener annotated method specifically for the integration test



回答2:

In order to avoid having to change your actual @JmsListener method, you could try and use AOP in your test...

First create an aspect class like this:

@Aspect
public static class JmsListenerInterceptor {
    @org.aspectj.lang.annotation.After("@annotation(org.springframework.jms.annotation.JmsListener)")
    public void afterOnMessage(JoinPoint jp) {
        // Do countdown latch stuff...
    }
}

Then add it in your application context configuration you're using for testing, like this:

<aop:aspectj-autoproxy/>
<bean id="jmsListenerInterceptor" class="path.to.your.Test$JmsListenerInterceptor" />

If everything goes as planned, the JmsListenerInterceptor will count down and you don't have to change your actual code.

IMPORTANT: I just found out that using AOP and Mockito to verify if certain methods in your @JmsListener have been called is a bad combination. The reason seems to be the extra wrapping into CGLib classes resulting in the wrong/actual target instance to be invoked instead of the Mockito proxy.

In my test, I have an @Autowired, @InjectMocks Listener object and a @Mock Facade object for which I want to verify that a certain method has been called.

With AOP:

  • Test Thread:
    • [JmsListenerTest] 2279812 - class Listener$$EnhancerBySpringCGLIB$$6587f46b (wrapped by Spring AOP)
    • [JmsListenerTest] 30960534 - class Facade$$EnhancerByMockitoWithCGLIB$$69fe8952 (wrapped by Mockito)
  • Listener Thread:
    • [Listener] 1151375 - class Listener (target instance of the AOP wrapped class)
    • [Listener] 4007155 - class FacadeImpl (not the actual instance we expected)

Without AOP:

  • Test Thread:
    • [JmsListenerTest] 10692528 - class Listener (actual instance)
    • [JmsListenerTest] 823767 - class Facade$$EnhancerByMockitoWithCGLIB$$773538e8 (wrapped by Mockito)
  • Listener Thread:
    • [Listener] 10692528 - class Listener (still actual instance)
    • [Listener] 823767 - class Facade$$EnhancerByMockitoWithCGLIB$$773538e8 (still our mocked instance)

This goes to show that you'll need to watch out using AOP the way I tried to, as you might end up with different instances in both Threads...



回答3:

If you would add logging to the @JmsListener annotated method, you could do something like this in the test class

@Rule
public OutputCapture outputCapture = new OutputCapture();

@Test
public void test() {
    sendSomeMessage();
    //how to wait here for the @JMSListener method to complete
    Assertions.assertThat(outputCapture.toString()).contains("Message received.");
}