Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing Spring ApplicationEvents - Events are getting published but the listeners aren't firing?

Tags:

spring

I'm trying to unit test the custom events that I've created in Spring and am running into an interesting problem. If I create a StaticApplicationContext and manually register and wire the beans I can trigger events and see the program flow through the publisher (implements ApplicationEventPublisherAware) through to the listener (implements ApplicationListener<?>).

Yet when I try to create a JUnit test to create the context using the SpringJunit4ClassRunner and @ContextConfiguration everything works well except that the ApplicationEvents are not showing up in the listener (I have confirmed that they are getting published).

Is there some other way to create the context so that ApplicationEvents will work correctly? I haven't found much on the web about unit testing the Spring events framework.

like image 489
jcurtin Avatar asked Oct 26 '11 16:10

jcurtin


People also ask

How do I listen events in spring boot?

Annotation for Spring Boot event listeners A method that listens for spring boot events is created using @EventListener . The ApplicationEventPublisher class is used to broadcast a spring boot event. When an event is published with the ApplicationEventPublisher class, the @EventListener annotated methods are called.

Which method is used to publish your own custom event in spring?

To publish custom events, we will need an instance of ApplicationEventPublisher and then call the method ApplicationEventPublisher#publishEvent(..) . Another way to publish event is to use ApplicationContext#publishEvent(....) .

Are spring events asynchronous?

Annotation-Driven Event Listener By default, the listener is invoked synchronously. However, we can easily make it asynchronous by adding an @Async annotation. We just need to remember to enable Async support in the application.


1 Answers

The events will not fire because your test classes are not registered and resolved from the spring application context, which is the event publisher.

I've implemented a workaround for this where the event is handled in another class that is registered with Spring as a bean and resolved as part of the test. It isn't pretty, but after wasting the best part of a day trying to find a better solution I am happy with this for now.

My use case was firing an event when a message is received within a RabbitMQ consumer. It is made up of the following:

The wrapper class

Note the Init() function that is called from the test to pass in the callback function after resolving from the container within the test

public class TestEventListenerWrapper {

CountDownLatch countDownLatch;
TestEventWrapperCallbackFunction testEventWrapperCallbackFunction;

public TestEventListenerWrapper(){

}

public void Init(CountDownLatch countDownLatch, TestEventWrapperCallbackFunction testEventWrapperCallbackFunction){

    this.countDownLatch = countDownLatch;
    this.testEventWrapperCallbackFunction = testEventWrapperCallbackFunction;
}

@EventListener
public void onApplicationEvent(MyEventType1 event) {

    testEventWrapperCallbackFunction.CallbackOnEventFired(event);
    countDownLatch.countDown();
}

@EventListener
public void onApplicationEvent(MyEventType2 event) {

    testEventWrapperCallbackFunction.CallbackOnEventFired(event);
    countDownLatch.countDown();
}

@EventListener
public void onApplicationEvent(OnQueueMessageReceived event) {

    testEventWrapperCallbackFunction.CallbackOnEventFired(event);
    countDownLatch.countDown();
}
}

The callback interface

public interface TestEventWrapperCallbackFunction {

void CallbackOnEventFired(ApplicationEvent event);
}

A test configuration class to define the bean which is referenced in the unit test. Before this is useful, it will need to be resolved from the applicationContext and initialsed (see next step)

    @Configuration
public class TestContextConfiguration {
    @Lazy
    @Bean(name="testEventListenerWrapper")
    public TestEventListenerWrapper testEventListenerWrapper(){
        return new TestEventListenerWrapper();
    }
}

Finally, the unit test itself that resolves the bean from the applicationContext and calls the Init() function to pass assertion criteria (this assumes you have registered the bean as a singleton - the default for the Spring applicationContext). The callback function is defined here and also passed to Init().

    @ContextConfiguration(classes= {TestContextConfiguration.class,
                                //..., - other config classes
                                //..., - other config classes
                                })
public class QueueListenerUnitTests
        extends AbstractTestNGSpringContextTests {

    private MessageProcessorManager mockedMessageProcessorManager;
    private ChannelAwareMessageListener queueListener;

    private OnQueueMessageReceived currentEvent;

    @BeforeTest
    public void Startup() throws Exception {

        this.springTestContextPrepareTestInstance();
        queueListener = new QueueListenerImpl(mockedMessageProcessorManager);
        ((QueueListenerImpl) queueListener).setApplicationEventPublisher(this.applicationContext);
        currentEvent = null;
    }

    @Test
    public void HandleMessageReceived_QueueMessageReceivedEventFires_WhenValidMessageIsReceived() throws Exception {

        //Arrange
        //Other arrange logic
        Channel mockedRabbitmqChannel = CreateMockRabbitmqChannel();
        CountDownLatch countDownLatch = new CountDownLatch(1);

        TestEventWrapperCallbackFunction testEventWrapperCallbackFunction = (ev) -> CallbackOnEventFired(ev);
        TestEventListenerWrapper testEventListenerWrapper = (TestEventListenerWrapper)applicationContext.getBean("testEventWrapperOnQueueMessageReceived");
        testEventListenerWrapper.Init(countDownLatch, testEventWrapperCallbackFunction);

        //Act
        queueListener.onMessage(message, mockedRabbitmqChannel);
        long awaitTimeoutInMs = 1000;
        countDownLatch.await(awaitTimeoutInMs, TimeUnit.MILLISECONDS);

        //Assert - assertion goes here
    }

    //The callback function that passes the event back here so it can be made available to the tests for assertion
    private void CallbackOnEventFired(ApplicationEvent event){
        currentEvent = (OnQueueMessageReceived)event;
    }
}
  • EDIT 1: The sample code has been updated with CountDownLatch
  • EDIT 2: Assertions didn't fail tests so the above was updated with a different approach**
like image 90
sean_mufc Avatar answered Nov 05 '22 00:11

sean_mufc