Ippon Blog

Stop snoozing your tests!

Written by Ben Scott | Sep 8, 2020 12:56:00 PM

The example project I'll be using as a demo is a simple Spring Boot project and will use Spring's application event publisher as our example message bus.

Message publisher

@Service
public class MessageService {

    ApplicationEventPublisher eventPublisher;

    public MessageService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Async
    public void publishMessage(Message message) {
        Random rand = new Random();
        try {
            Thread.sleep(rand.nextInt(4000));
            eventPublisher.publishEvent(message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

This is our simple event publisher, it takes a Message object, sleeps a random amount between 0 and 4 seconds and publishes the message. The sleep here is done for demonstration purposes and simulates something that takes a while to process so that the asynchronous method can return immediately to our test case.

Testing

For testing this we will need two things: a test consumer, and a Spring Boot test.

Our test consumer will be a Spring component that lives in our test package. This is something often overlooked by developers. You can create custom Spring components that will not be packaged alongside your production code.

@Component
public class MessageConsumer {

    Message resultMessage;
    CountDownLatch countDownLatch;

    @EventListener
    public void consumeMessage(Message message) {
        resultMessage = message;
        countDownLatch.countDown();
    }

    public void resetMessage() {
        resultMessage = null;
    }

    public void resetCountDownLatch() {
        countDownLatch = new CountDownLatch(1);
    }

    public Message getResultMessage() {
        return resultMessage;
    }

    public CountDownLatch getCountDownLatch() {
        return countDownLatch;
    }
}

Our event listener does two things, it saves the content of the message to a class variable so that it's accessible via a getter, and it count downs our CountDownLatch. A CountDownLatch is a blocking mechanism used to synchronize threads so that 1 or more threads can resume based on when the CountDownLatch goes to 0. In our case we block our test thread when we publish the message, and it our consumer thread countdowns the latch when the event is processed allowing our test thread to resume and assert the result.

Finally our test:

@SpringBootTest
class MessageServiceTest {

    @Autowired
    private MessageService messageService;

    @Autowired
    private MessageConsumer messageConsumer;

    @BeforeEach
    public void beforeEach() {
        messageConsumer.resetMessage();
        messageConsumer.resetCountDownLatch();
    }

    @Test
    public void publishMessage() throws InterruptedException {

        Message message = new Message("source", "Hello World!");
        messageService.publishMessage(message);

        messageConsumer.getCountDownLatch().await(4, TimeUnit.SECONDS);

        assertNotNull(message);
        assertEquals(message.getMessage(), messageConsumer.getResultMessage().getMessage());
        assertEquals(message.getName(), messageConsumer.getResultMessage().getName());
    }
}

As we're using Spring's events we need to use @SpringBootTest to load up the Spring context. We also need two beans in this test, the subject of our test and our test helper.

Our test case is simple:

  • We publish a message
  • We wait up to 4 seconds by locking the thread with the CountDownLatch
  • We assert that our message is what we expect

The advantage of using the CountDownLatch versus Thread.sleep() here is that the Latch will release the thread as soon as the message is consumed by our TestConsumer so our test can start asserting immediately.

The other advantage is we can control our the CountDownLatch's timeout to reflect the SLA of our system. For example, if we expect our publisher to be able to produce the message within 4 seconds and we receive the message after 5 seconds then the test failure points to a performance issue.

The code for this blog can be found here.