Как дождаться завершения аннотированного метода @JMSListener в JUnit
Поэтому я пытаюсь провести интеграционное тестирование обработки JMS, кода на основе Spring (v4.1.6).
Это очень стандартная установка Spring с @JmsListener
аннотированный метод и DefaultMessageListenerContainer
с concurrency
установлен в 1
и, следовательно, разрешить только 1 поток прослушивания.
Теперь я использовал встроенный брокер ActiveMQ, чтобы не полагаться на какого-либо внешнего брокера jms для выполнения тестов в любом месте в любое время (я должен работать в маркетинге).
Так что все провода хорошо, а затем у меня есть мой тест JUnit:
@Test
public void test() {
sendSomeMessage();
//how to wait here for the @JMSListener method to complete
verify();
}
Я отправляю сообщение, но тогда мне нужно как-то дождаться @JMSListener
аннотированный метод для завершения. Как мне это сделать?
3 ответа
Что ж, я надеялся, что смогу каким-то образом подключиться к жизненному циклу Message Driven Pojos, но, пройдя другие SO вопросы об асинхронном коде, я нашел решение, основанное на CountDownLatch
@JMSListener
аннотированный метод должен вызыватьcountDown()
на CountDownLatch после того, как вся работа завершена:@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(); } }
В тестовом методе
@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(); }
Недостатком этого решения является то, что вы должны изменить @JMSListener
аннотированный метод специально для интеграционного теста
Если бы вы добавили запись в аннотированный метод @JmsListener, вы можете сделать что-то подобное в тестовом классе
@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.");
}
Чтобы избежать необходимости изменять ваш фактический метод @JmsListener, вы можете попробовать использовать AOP в своем тесте...
Сначала создайте класс аспекта следующим образом:
@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...
}
}
Затем добавьте его в конфигурацию контекста вашего приложения, которую вы используете для тестирования, например так:
<aop:aspectj-autoproxy/>
<bean id="jmsListenerInterceptor" class="path.to.your.Test$JmsListenerInterceptor" />
Если все пойдет по плану, JmsListenerInterceptor начнет обратный отсчет, и вам не нужно будет менять свой фактический код.
ВАЖНО: Я только что узнал, что использование AOP и Mockito для проверки, были ли вызваны определенные методы в вашем @JmsListener, является плохой комбинацией. Причина, по-видимому, заключается в дополнительном переносе в классы CGLib, в результате которого вместо прокси-сервера Mockito вызывается неверный / фактический целевой экземпляр.
В моем тесте у меня есть объект @Autowired, @InjectMocks Listener и объект @Mock Facade, для которого я хочу убедиться, что был вызван определенный метод.
С АОП:
- Тестовая тема:
- [JmsListenerTest] 2279812 - прослушиватель класса $$ EnhancerBySpringCGLIB $$ 6587f46b (упакован Spring AOP)
- [JmsListenerTest] 30960534 - класс Facade $$ EnhancerByMockitoWithCGLIB $$ 69fe8952 (завернутый в Mockito)
- Поток слушателя:
- [Listener] 1151375 - класс Listener (целевой экземпляр обернутого класса AOP)
- [Listener] 4007155 - класс FacadeImpl (не фактический экземпляр, который мы ожидали)
Без АОП:
- Тестовая тема:
- [JmsListenerTest] 10692528 - Слушатель класса (фактический экземпляр)
- [JmsListenerTest] 823767 - класс Facade $$ EnhancerByMockitoWithCGLIB $$ 773538e8 (завернутый в Mockito)
- Поток слушателя:
- [Listener] 10692528 - класс Listener (все еще актуальный экземпляр)
- [Listener] 823767 - класс Facade $$ EnhancerByMockitoWithCGLIB $$ 773538e8 (все еще наш поддельный экземпляр)
Это говорит о том, что вам нужно следить за использованием AOP так, как я пытался, поскольку у вас могут быть разные экземпляры в обоих потоках...
Я использую пружинные профили и использую другой Processor
в тестах против производственного кода. В моем тестовом коде я пишу вBlockingQueue
после обработки, которую можно дождаться в тесте
Например:
@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);
}
}