How to test Spring ApplicationEventPublisher

How to test Spring Events

1. Overview

The event publisher ApplicationEventPublisher in Spring is a class that allows you to publish generic domain event objects internal to your application and have @EventListener decorated methods handle to them synchronously.

In this article, we will be taking a behavior-driven development style in testing the publication of certain events. To do this we’ll be using the Java Mocking framework, Mockito to verify that our events are actually being published.

2. Creating a publisher

First, we’ll create a service class that publishes our events. In this example we have a simple counter service that counts to a given number and publishes start, progress changed and finished events. Inject the ApplicationEventPublisher provided by Spring.

2.1 A service class

@Service
public class CounterService {

    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    public CounterService(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void count(int times) {
        applicationEventPublisher.publishEvent(new CountStartedEvent(times));

        for (int i = 1; i <= times; i++) {
            applicationEventPublisher.publishEvent(new CountProgressChanged(i));
        }

        applicationEventPublisher.publishEvent(new CountFinishedEvent(times));
    }
}

2.2 An event example

Create a simple event class. In this example, we create the CountProgressChanged event

public class CountProgressChanged {
    private int count;

    public CountProgressChanged(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    @Override
    public String toString() {
        return "CountProgressChanged{" +
                "count=" + count +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CountProgressChanged that = (CountProgressChanged) o;

        return count == that.count;
    }

    @Override
    public int hashCode() {
        return getCount();
    }
}

3. Unit testing

Our first test example is a plain old unit test without using the @SpringBootTest annotation to load up the Spring context.

3.1 Simple Mockito test

@Mock
private ApplicationEventPublisher applicationEventPublisher;

@InjectMocks
private CounterService counterService;

@Captor
protected ArgumentCaptor<Object> publishEventCaptor;

@Before
public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
}

@Test
public void testCounter_countTo3_publishesEvents() {
    counterService.count(3);

    CountStartedEvent countStartedEvent = new CountStartedEvent(3);
    CountProgressChanged progressChanged1 = new CountProgressChanged(1);
    CountProgressChanged progressChanged2 = new CountProgressChanged(2);
    CountProgressChanged progressChanged3 = new CountProgressChanged(3);
    CountFinishedEvent countFinishedEvent = new CountFinishedEvent(3);

    verifyPublishedEvents(countStartedEvent, progressChanged1, progressChanged2, progressChanged3, countFinishedEvent);
}

protected void verifyPublishedEvents(Object... events) {
    Mockito.verify(applicationEventPublisher, Mockito.times(events.length)).publishEvent(publishEventCaptor.capture());
    List<Object> capturedEvents = publishEventCaptor.getAllValues();

    for (int i = 0; i < capturedEvents.size(); i++) {
        Assert.assertThat(capturedEvents.get(i), instanceOf(events[i].getClass()));
        System.out.println(events[i].getClass().getName());
        Assert.assertEquals(capturedEvents.get(i), events[i]);
    }
}

3.2 Spring Boot test

In this example, we create a @SpringBootTest test that will load up the entire Spring Context. To mock a bean object in Spring, use the @MockBean annotation. Unfortunately, we cannot use the convenient Mockito @InjectMocks annotation to inject our Spring dependencies into the CounterService because Mockito isn’t designed to handle complex dependency injection.

We can’t inject the CounterService into our test with the @Autowired Spring annotation because Spring will load a default ApplicationEventPublisher bean before our @MockBean is created. The only way to overcome this is to create a @Bean ApplicationEventPublisher yourself as a Mockito Mock

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

    @MockBean
    private ApplicationEventPublisher applicationEventPublisher;

    private CounterService counterService;

    @Captor
    protected ArgumentCaptor<Object> publishEventCaptor;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        counterService = new CounterService(applicationEventPublisher);
    }

    @Test
    public void testCounter_countTo3_publishesEvents() {
        counterService.count(3);

        CountStartedEvent countStartedEvent = new CountStartedEvent(3);
        CountProgressChanged progressChanged1 = new CountProgressChanged(1);
        CountProgressChanged progressChanged2 = new CountProgressChanged(2);
        CountProgressChanged progressChanged3 = new CountProgressChanged(3);
        CountFinishedEvent countFinishedEvent = new CountFinishedEvent(3);

        verifyPublishedEvents(countStartedEvent, progressChanged1, progressChanged2, progressChanged3, countFinishedEvent);
    }

    protected void verifyPublishedEvents(Object... events) {
        Mockito.verify(applicationEventPublisher, Mockito.times(events.length)).publishEvent(publishEventCaptor.capture());
        List<Object> capturedEvents = publishEventCaptor.getAllValues();

        for (int i = 0; i < capturedEvents.size(); i++) {
            Assert.assertThat(capturedEvents.get(i), instanceOf(events[i].getClass()));
            System.out.println(events[i].getClass().getName());
            Assert.assertEquals(capturedEvents.get(i), events[i]);
        }
    }
}

4. Conclusion

There you have it. In this tutorial we covered how to test the Spring event publisher in two different ways – either testing with plain unit tests and second creating integration-unit tests that spin up the application context and injecting a mock bean.

Adrian van den Houten

I'm Adrian van Houten, founder of ScholarCoder and a passionate software developer for full-stack web development. Read more about me here.