Testing Google Guava EventBus

Recently I have used Google Guava EventBus in one of my projects. It worked pretty well but testing it was kind of a challenge.

If you are not familiar with the EventBus you can learn more about it here.

You can find some code for this article at github

Why?

First things first, so let me explain in short words why one would consider using this EventBus.

Let say you have a service which should talk with another service. Usually the code looks like this:

public void doSomething(...) {
   ... some code here
   anotherService.performSomeAction(...)
}

This time there is only one collaborator, but we could imagine more of them.

Sometimes I would like weaker coupling between services. A publisher/subscriber pattern (or observer, or whatever you call it) is what comes to my mind. If you followed this pattern (and use EventBus) you would end up with the following code:

public void doSomething(...) {
   ... some code here
   eventBus.post(new DoneSomethingEvent(...))
}

You would also need a separate listener like this:

@Subscribe
public void listenToDoneEvents(DoneSomethingEvent event) {
   anotherService.performSomeAction(...)
}

Now, let us assume that from the architectural point of view, this is what we wanted. The question is how could we test such code?

Wrapper

But before we write any test, let me stress one thing. We do not touch EventBus directly. Instead, we create a thin wrapper over it and call it from our services.

class EventBusWrapper {

    private EventBus eventBus; // injected somehow

    public void post(Event event) {
        eventBus.post(event);
    }

    public void registerListener(Listener listener)
        eventBus.register(listener);
    }
}

There is a good reason for this additional, and seemingly unnecessary, class. It allows us to follow the "mock only the types you own" and also makes our code more resilient to change (i.e. if we change EventBus to something else, there is a fat chance the only thing we need to update is our thin wrapper).

Unit Testing

In short: piece of cake. Classes which listen to events distributed by the EventBus are "normal". Methods, which will receive events from the EventBus are marked with the @Subscriber annotation. And that is all. Which means that you can test them using standard unit testing techniques, i.e. mocking all collaborators and verifying their logic.

If you want an example, refer to an article by Tomasz Dziurko.

Similarly, testing of services which publish events to the EventBus is also straightforward. Since the instance of the EventBus wrapper is injected via DI, there is no problem with mocking it.

But Does It Work?

Unit testing is sweet, but does the whole thing really work? Does a event of some type will be delivered to the right listeners?

This is not so simple to verify. At least, I do not have a good answers. Only some ideas which I would like to share with you in hope that you:

  • have better ideas and would be kind to share them with me,
  • make some use of my ideas.

I See DeadEvents

My first idea was to use DeadEvent. This is an interesting feature of EventBus. If you register a listener which accepts events of class DeadEvent then it will receive all events which were not handled by any other listener.

A test could look like this:

@ContextConfiguration(locations = {"classpath:listeners.xml"})
public class EventsItTest extends AbstractTestNGSpringContextTests {

    @Autowired
    EventBusWrapper eventBus;

    @Test
    public void shouldCatchAllImportantBusinessEvents() {
        DeadEventListener deadEventsCollector =  new DeadEventListener();
        eventBus.register(deadEventsCollector);

        // when
        eventBus.post(new DoneThisEvent());
        eventBus.post(new DoneThatEvent());

        // then
        assertThat(deadEventsCollector.getEvents()).isEmpty();
    }

    private class DeadEventListener {

        private Set<DeadEvent> events = new HashSet<DeadEvent>();

        @Subscribe
        public void fetchDeadEvents(DeadEvent event) {
            events.add(event);
        }

        public Set<DeadEvent> getEvents() {
            return events;
        }
    }
}

Hmm... not really. This is testing something, but not exactly what we wanted. If we had some generic listener - i.e. one which would catch all events and log them - then this test would always pass - even if there was no "real" listener catching business events. So this was a nice try, but we need to look for something else.

Springockito

Let us try a different approach. As previously it will be an integration test, but with mocks. We will test exactly whether a specific service is called when an event of given type is being posted via the EventBus.

Let the code speak.

@ContextConfiguration(
        loader = SpringockitoContextLoader.class,
        locations = {"classpath:listeners.xml"})
public class CallAnotherServiceItTest extends AbstractTestNGSpringContextTests {

    @ReplaceWithMock
    @Autowired
    private AnotherService anotherService;

    @Autowired
    EventBusWrapper eventBus;

    @Test
    public void shouldCallAnotherService() {
        // given
        // when
        eventBus.post(new DoneThisEvent());

        // then
        verify(anotherService).doSomething();
    }
}

So we load a Spring context from the listeners.xml configuration file, but we make one change there. Namely, we request Springockito (have you noticed the the SpringockitoContextLoader class used?) to replace one of the services with a mock (via the @ReplaceWithMock annotation). And later on, we do what we usually do with mocks.

This way we can see that posting an event of the DoneThisEvent type to the EventBus results in some business action (that we expected).

I believe this is much better than this DeadEvent testing idea. The only downside is that the test is kind of slow - i.e. it requires to load the Spring context which can take some time in real-world application.

P.S. And if you are looking for an event bus, you better take a look at Java event bus library comparison (not sure if still relevant though - was written in 2012).

Please comment using