如何为@RabbitListener注释编写集成测试?

时间:2015-12-24 14:19:24

标签: spring-amqp spring-rabbit

我的问题实际上是一个跟进问题

RabbitMQ Integration Test and Threading

它声明要包装你的听众"并传入CountDownLatch,最终所有线程都将合并。如果我们手动创建和注入消息监听器但是对于@RabbitListener注释,这个答案是有效的...我不确定如何传入CountDownLatch。该框架在幕后自动神奇地创建了消息监听器。

还有其他方法吗?

2 个答案:

答案 0 :(得分:2)

在@Gary Russell的帮助下,我得到了答案并使用了以下解决方案。

结论:我必须承认我对这个解决方案漠不关心(感觉就像一个黑客)但这是我唯一可以开始工作的东西,一旦你克服了最初的一次设置和实际上理解“工作流程”并不是那么痛苦。基本上归结为定义(2)@Beans并将它们添加到Integration Test配置中。

下面发布的解决方案示例解决方案。请随时建议改进此解决方案。

1。定义一个ProxyListenerBPP,在Spring初始化期间将监听指定的clazz(即我们的包含@RabbitListener的测试类)和 注入我们在下一步中定义的自定义CountDownLatchListenerInterceptor建议。

import org.aopalliance.aop.Advice;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;

/**
 * Implements BeanPostProcessor bean... during spring initialization we will
 * listen for a specified clazz 
 * (i.e our @RabbitListener annotated class) and 
 * inject our custom CountDownLatchListenerInterceptor advice
 * @author sjacobs
 *
 */
public class ProxyListenerBPP implements BeanPostProcessor, BeanFactoryAware, Ordered, PriorityOrdered{

    private BeanFactory beanFactory;
    private Class<?> clazz;
    public static final String ADVICE_BEAN_NAME = "wasCalled";

    public ProxyListenerBPP(Class<?> clazz) {
        this.clazz = clazz;
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (clazz.isAssignableFrom(bean.getClass())) {
            ProxyFactoryBean pfb = new ProxyFactoryBean();
            pfb.setProxyTargetClass(true); // CGLIB, false for JDK proxy (interface needed)
            pfb.setTarget(bean);
            pfb.addAdvice(this.beanFactory.getBean(ADVICE_BEAN_NAME, Advice.class));
            return pfb.getObject();
        }
        else {
            return bean;
        }
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1000; // Just before @RabbitListener post processor
    }

2. :创建将保存对CountDownLatch的引用的MethodInterceptor通知impl。需要在Integration测试线程和@RabbitListener中的异步工作线程内引用CountDownLatch。因此,我们可以在@RabbitListener异步线程完成执行后立即释放回集成测试线程 。无需投票。

import java.util.concurrent.CountDownLatch;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
 * AOP MethodInterceptor that maps a <b>Single</b> CountDownLatch to one method and invokes 
 * CountDownLatch.countDown() after the method has completed execution. The motivation behind this 
 * is for integration testing purposes of Spring RabbitMq Async Worker threads to be able to merge
 * the Integration Test thread after an Async 'worker' thread completed its task. 
 * @author sjacobs
 *
 */
public class CountDownLatchListenerInterceptor implements MethodInterceptor {

    private CountDownLatch  countDownLatch =  new CountDownLatch(1);

    private final String methodNameToInvokeCDL ;

    public CountDownLatchListenerInterceptor(String methodName) {
        this.methodNameToInvokeCDL = methodName;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();

        if (this.methodNameToInvokeCDL.equals(methodName) ) {

            //invoke async work 
            Object result = invocation.proceed();

            //returns us back to the 'awaiting' thread inside the integration test
            this.countDownLatch.countDown();

            //"reset" CountDownLatch for next @Test (if testing for more async worker)
            this.countDownLatch = new CountDownLatch(1);

            return result;
        } else
            return invocation.proceed();
    }


    public CountDownLatch getCountDownLatch() {
        return countDownLatch;
    }
}

3. 接下来添加到您的Integration Test配置以下@Bean(s)

public class SomeClassThatHasRabbitListenerAnnotationsITConfig extends BaseIntegrationTestConfig {

    // pass into the constructor the test Clazz that contains the @RabbitListener annotation into the constructor
    @Bean
    public static ProxyListenerBPP listenerProxier() { // note static
        return new ProxyListenerBPP(SomeClassThatHasRabbitListenerAnnotations.class);
    }

     // pass the method name that will be invoked by the async thread in SomeClassThatHasRabbitListenerAnnotations.Class
    // I.E the method name annotated with @RabbitListener or @RabbitHandler
    // in our example 'listen' is the method name inside SomeClassThatHasRabbitListenerAnnotations.Class
    @Bean(name=ProxyListenerBPP.ADVICE_BEAN_NAME)
    public static Advice wasCalled() {
        String methodName = "listen";  
        return new CountDownLatchListenerInterceptor( methodName );
    }

    // this is the @RabbitListener bean we are testing
    @Bean
    public SomeClassThatHasRabbitListenerAnnotations rabbitListener() {
         return new SomeClassThatHasRabbitListenerAnnotations();
    }

}

4. 最后,在集成@Test调用...通过rabbitTemplate发送消息后触发异步线程...现在调用CountDownLatch #await(...)方法获得来自拦截器,并确保传入一个TimeUnit args,以便在长时间运行的过程中出现故障或出现问题。一旦异步,集成测试线程被通知(唤醒),现在我们终于可以开始实际测试/验证/验证异步工作的结果。

@ContextConfiguration(classes={ SomeClassThatHasRabbitListenerAnnotationsITConfig.class } )
public class SomeClassThatHasRabbitListenerAnnotationsIT extends BaseIntegrationTest{

    @Inject 
    private CountDownLatchListenerInterceptor interceptor;

    @Inject
    private RabbitTemplate rabbitTemplate;

    @Test
    public void shouldReturnBackAfterAsyncThreadIsFinished() throws Exception {

     MyObject payload = new MyObject();
     rabbitTemplate.convertAndSend("some.defined.work.queue", payload);
        CountDownLatch cdl = interceptor.getCountDownLatch();      

        // wait for async thread to finish
        cdl.await(10, TimeUnit.SECONDS);    // IMPORTANT: set timeout args. 

        //Begin the actual testing of the results of the async work
        // check the database? 
        // download a msg from another queue? 
        // verify email was sent...
        // etc... 
}

答案 1 :(得分:1)

使用@RabbitListener会有点棘手,但最简单的方法是建议听众。

使用custom listener container factory只需将测试用例添加到工厂。

建议是MethodInterceptor;调用将有2个参数;频道和(未转换的)Message。必须在创建容器之前注入建议。

或者,使用registry获取对容器的引用并稍后添加建议(但您必须调用initialize()以强制应用新建议。)

替代方法是在将侦听器类注入容器之前代理侦听器类的简单BeanPostProcessor。这样,您将看到任何转换后的方法论者;您还可以验证侦听器返回的任何结果(对于请求/回复方案)。

如果您不熟悉这些技巧,我可以尝试找一些时间为您启动快速示例。

修改

我发了pull requestEnableRabbitIntegrationTests添加示例。这会添加一个带有2个带注释的侦听器方法的侦听器bean,BeanPostProcessor代理侦听器bean,然后将其注入侦听器容器。向代理添加Advice,在收到预期消息时对锁存进行计数。