单元测试 Spring ApplicationEvents - 事件正在发布,但侦听器没有触发?

发布于 2024-12-11 22:33:50 字数 439 浏览 0 评论 0原文

我正在尝试对我在 Spring 中创建的自定义事件进行单元测试,但遇到了一个有趣的问题。如果我创建一个StaticApplicationContext并手动注册和连接bean,我可以触发事件并查看从发布者(实现ApplicationEventPublisherAware)到监听器(实现>ApplicationListener)。

然而,当我尝试创建 JUnit 测试来使用 SpringJunit4ClassRunner 和 @ContextConfiguration 创建上下文时,一切正常,除了 ApplicationEvents 没有显示在侦听器中(我有确认它们正在发布)。

是否有其他方法来创建上下文以便 ApplicationEvents 能够正常工作?我在网上没有找到太多关于 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

随梦而飞# 2024-12-18 22:33:50

这些事件不会触发,因为您的测试类没有从 Spring 应用程序上下文(即事件发布者)注册和解析。

我已经为此实现了一种解决方法,其中事件在另一个类中处理,该类作为 bean 向 Spring 注册并作为测试的一部分进行解析。这并不漂亮,但在浪费了一天中最好的时间试图找到更好的解决方案之后,我现在对此感到满意。

我的用例是当 RabbitMQ 消费者收到消息时触发一个事件。它由以下部分组成:

包装类

注意从测试中调用的 Init() 函数,该函数在从容器内解析后传入回调函数。 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();
}
}

回调接口

public interface TestEventWrapperCallbackFunction {

void CallbackOnEventFired(ApplicationEvent event);
}

测试配置类,用于定义单元测试中引用的bean。在此之前,需要从 applicationContext 解析并初始化它(请参阅下一步)。

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

最后,单元测试本身从 applicationContext 解析 bean 并调用 Init() 函数来传递断言标准(这假设您已将 bean 注册为单例 - Spring applicationContext 的默认值)。回调函数在此处定义,并传递给 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;
    }
}
  • 编辑 1:示例代码已使用 CountDownLatch 更新
  • 编辑 2:断言测试没有失败,因此上面的内容已使用不同的方法进行了更新**

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**
谈场末日恋爱 2024-12-18 22:33:50

我只是将我的应用程序作为 SpringBootTest 运行,应用程序事件工作正常:

@TestComponent
public class EventTestListener {

    @EventListener
    public void handle(MyCustomEvent event) {
        // nothing to do, just spy the method...
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyEventTest {

    @SpyBean
    private EventTestListener testEventListener;

    @Test
    public void testMyEventFires() {
        // do something that fires the event..

        verify(testEventListener).handle(any(MyCustomEvent.class));
    }
}

使用 @Captor / ArgumentCaptor 来验证事件的内容。

I just run my app as SpringBootTest, application events working fine:

@TestComponent
public class EventTestListener {

    @EventListener
    public void handle(MyCustomEvent event) {
        // nothing to do, just spy the method...
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyEventTest {

    @SpyBean
    private EventTestListener testEventListener;

    @Test
    public void testMyEventFires() {
        // do something that fires the event..

        verify(testEventListener).handle(any(MyCustomEvent.class));
    }
}

use the @Captor / ArgumentCaptor to verify the content of your event.

落花浅忆 2024-12-18 22:33:50

您可以手动创建上下文。

例如:我需要检查我的 ApplicationListener 是否关闭了 Cassandra 连接:

@Test
public void testSpringShutdownHookForCassandra(){
    ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CassandraConfig.class);

    CassandraConnectionManager connectionManager = ctx.getBean(CassandraConnectionManager.class);
    Session session = connectionManager.openSession(testKeySpaceName);

    Assert.assertFalse( session.isClosed() );
    ctx.close();

    Assert.assertTrue( session.isClosed() );
}

You can create a context manually.

For example: I had needed to check if my ApplicationListener<ContextClosedEvent> closed Cassandra connections:

@Test
public void testSpringShutdownHookForCassandra(){
    ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CassandraConfig.class);

    CassandraConnectionManager connectionManager = ctx.getBean(CassandraConnectionManager.class);
    Session session = connectionManager.openSession(testKeySpaceName);

    Assert.assertFalse( session.isClosed() );
    ctx.close();

    Assert.assertTrue( session.isClosed() );
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文