How to wait for @JMSListener annotated method to complete in JUnit

前端 未结 4 579
失恋的感觉
失恋的感觉 2021-01-22 03:50

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

相关标签:
4条回答
  • 2021-01-22 04:20

    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.");
    }
    
    0 讨论(0)
  • 2021-01-22 04:25

    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

    0 讨论(0)
  • 2021-01-22 04:38

    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);
       }
    
    }
    
    0 讨论(0)
  • 2021-01-22 04:45

    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...

    0 讨论(0)
提交回复
热议问题