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?
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
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();
}
}
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
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...
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.");
}