Mockito’s ArgumentCaptor meets AsyncWordSpec

ArgumentCaptor

ArgumentCaptor – ask what is passed by

ArgumentCaptor is a nice Mockito feature that can be used to check and validate arguments passed by inside tested portion of code. With that feature we can see what happens internally – we can check ‘what’s inside the box.’

Background

Let’s suppose you have several kinds of messages you are passing by in your system and you want to persist corresponding types of event in something we would call an event log. That will be our model for Event:

sealed trait Event

case object EventA        extends Event
case object EventB        extends Event
case object EventC        extends Event
case object EventUnknown  extends Event

We also have conversion from messages into Event types:

object EventCreator {
  def fromMessage: String => Event = {
    case "messageA" => EventA
    case "messageB" => EventB
    case "messageC" => EventC
    case _          => EventUnknown
  }
}

So whenever somewhere you receive “messageA” statement you want to store corresponding event. As it was said before – events will be stored in the event log:

trait EventRepository {
  def save(event: Event): Future[Unit]
}

Use case

We have a trait defined that describes EventPersistorbehavior. An implementation should – for given String message – persist appropiate Event in the event log:

import scala.concurrent.Future

trait EventPersistor {
  def persistFor(message: String): Future[Unit]
}

Let us provide example implementation. We will use the conversion from EventCreator:

import [...].EventCreator.fromMessage

import scala.concurrent.Future

class EventPersistorImpl(eventRepository: EventRepository) extends EventPersistor {
  override def persistFor(message: String): Future[Unit] = {
    val event = fromMessage(message)
    eventRepository.save(event)
  }
}

As you can see we create event in val event = fromMessage(message) and persist in eventRepository.save(event). Those two lines, especially one with the save invocation, are crucial as we hope that correct event will be created and then passed by to function. Still nothing complicated happens here.

Testing

Using AsyncWordSpecwe can easily test whether our implementation really works. First of all, we have to mock our event log. Mockito smoothly help us:

private val mockedEventRepo = mock[EventRepository]
when(mockedEventRepo.save(any[Event]))
  .thenReturn(Future.successful())

Then, we create an instance of our implementation:

private val testedEventPersistorImpl = new EventPersistorImpl(mockedEventRepo)

Actual testing would look like:

testedEventPersistorImpl
  .persistFor("messageA")
  .map { result =>
    result shouldBe (())
}

Is that’s all? Well, not really… Cautious reader should notice that if above test passes it will only mean some kind of Event has been successfully saved. But we have no clue what exactly type of event was internally persisted. Actually, above test will pass for any message converted to any  Event. That’s definitely unwanted feature.

Star of the evening

Here ArgumentCaptor comes to the rescue. Firstly, we create instance of captor:

private val captor: ArgumentCaptor[Event] = ArgumentCaptor.forClass(classOf[Event])

Then, with a helper function:

private def checkPassedEvent(message: String, expectedEvent: Event) = {
    testedEventPersistorImpl
      .persistFor(message)
      .map { result =>
        result shouldBe (())
        verify(mockedEventRepo, atLeastOnce)
          .save(captor.capture())
        val passedEvent = captor.getValue
        passedEvent shouldBe expectedEvent
      }
}

we can finally test that our implementation really persists correct events:

checkPassedEvent(message = "messageA",    expectedEvent = EventA)
checkPassedEvent(message = "messageB",    expectedEvent = EventB)
checkPassedEvent(message = "messageC",    expectedEvent = EventC)
checkPassedEvent(message = "someMessage", expectedEvent = EventUnknown)

Reference

I provided you a lot of code snippets and not everything may be obvious at first reading. In case something is not clear enough or still doesn’t work you can get complete above example here: https://github.com/eltherion/mockito-argumentcaptor. Fork/clone it in order to play around and understand.