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
annotate
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.");
}
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
I use spring profiles and have a different Processor
in tests vs production code. In my test code, I write to a BlockingQueue
after processing which can be waited on in the test
Eg:
@Configuration
public class MyConfiguration {
@Bean @Profile("!test")
public Processor productionProcessor() {
return new ProductionProcessor();
}
@Bean @Profile("test")
public Processor testProcessor() {
return new TestProcessor();
}
@Bean
public MyListener myListener(Processor processor) {
return new MyListener(processor);
}
}
public class MyListener {
private final Processor processor;
// constructor
@JmsListener(destination = "dest", containerFactory = "cf")
public void processMessage(TextMessage message) throws JMSException {
processor.process(message);
}
}
public class TestProcessor extends ProductionProcessor {
private final BlockingQueue<TextMessage> queue = new LinkedBlockingQueue<>();
public void process(Textmessage message) {
super.process(message);
queue.add(message);
}
public BlockingQueue getQueue() { return queue; }
}
@SpringBootTest
@ActiveProfiles("test")
public class MyListenerTest {
@Autowired
private TestProcessor processor;
@Test
public void test() {
sendTestMessageOverMq();
TextMessage processedMessage = processor.getQueue().poll(10, TimeUnit.SECONDS);
assertAllOk(processedMessage);
}
}
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:
Without AOP:
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...