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).
- Tomek Kaczanowski's blog
- Login to post comments
This used to be my blog. I moved to http://tomek.kaczanowscy.pl long time ago.