Search code examples
scalaakkascalatestakka-testkit

Test that an akka actor receives a set of different types of messages without guaranteeing order


TL;DR:

I don't know how to test that an Akka actor receives a set of different types of messages without guaranteeing the order of the messages.

Specifics:

I'm testing out that some domain events are published to an akka.event.EventStream. In order to do so, I've subscribed a TestProbe to all the DomainEvent subclasses:

val eventBusTestSubscriber = TestProbe()(actorSystem)
actorSystem.eventStream.subscribe(eventBusTestSubscriber.ref, classOf[DomainEvent])

This way, I can test out that a single domain event arrives to the EventStream without taking into account other possible events (avoid fragile test):

Spec:

shouldPublishDomainEvent {
  event: WinterHasArrivedDomainEvent =>
    event.isReal shouldBe true
    event.year shouldBe expectedYear
}

Helper trait:

def shouldPublishDomainEvent[EventType](eventAsserter: EventType => Unit)
  (implicit gotContext: GotContextTest, classTag: ClassTag[EventType]): Unit = {

  val receivedEvent = gotContext.eventBusTestSubscriber.receiveN(1).head

  receivedEvent match {
    case event: EventType =>
      eventAsserter(event)

    case _ =>
      shouldPublishDomainEvent(eventAsserter)
  }
}

I also have some test for the scenarios in which I should receive a set of events of the same type guaranteeing the order without taking into account other possible events (avoid fragile test):

Spec:

val someoneDiedEventAsserter: SomeoneDiedDomainEvent => Unit = { event =>
  event.isReal shouldBe false
  event.episodeId shouldBe episodeId
}

val someoneDiedEventIdExtractor = (event: SomeoneDiedDomainEvent) => event.characterId

shouldPublishDomainEventsOfType(someoneDiedEventAsserter, someoneDiedEventIdExtractor)(characterIdsToDie)

Helper trait:

def shouldPublishDomainEventsOfType[EventType, EventIdType](
  eventAsserter: EventType => Unit,
  eventIdExtractor: EventType => EventIdType
)(expectedEventIds: Set[EventIdType])
  (implicit gotContext: GotContextTest, classTag: ClassTag[EventType]): Unit = {

  if (expectedEventIds.nonEmpty) {
    val receivedEvent = gotContext.eventBusTestSubscriber.receiveN(1).head

    receivedEvent match {
      case event: EventType =>
        eventAsserter(event)
        val receivedEventId = eventIdExtractor(event)
        expectedEventIds should contain(receivedEventId)
        shouldPublishDomainEventsOfType(eventAsserter, eventIdExtractor)(expectedEventIds - receivedEventId)

      case _ =>
        shouldPublishDomainEventsOfType(eventAsserter, eventIdExtractor)(expectedEventIds)
    }
  }
}

The problem now is with the use case in which I have to test that I'm publishing a set of events with different types and without the order guaranteed.

The problem I don't know how to solve is that, in the shouldPublishDomainEventsOfType case, I have an inferred EventType which provides me the type in order to perform the specific assertions related to this very specific type in the eventAsserter: EventType => Unit. But since I have different specific types of events, I don't know how to specify their types and so on.

I've tried an approach based on a case class containing the assertion function, but the issue is the same and I'm a little bit stuck:

case class ExpectedDomainEvent[EventType <: DomainEvent](eventAsserter: EventType => Unit)

def shouldPublishDomainEvents[EventType](
  expectedDomainEvents: Set[ExpectedDomainEvent]
)(implicit chatContext: ChatContextTest): Unit = {

  if (expectedDomainEvents.nonEmpty) {
    val receivedEvent = chatContext.eventBusTestSubscriber.receiveN(1).head

    expectedDomainEvents.foreach { expectedDomainEvent =>

      val wasTheReceivedEventExpected = Try(expectedDomainEvent.eventAsserter(receivedEvent))

      if (wasTheReceivedEventExpected.isSuccess) {
        shouldPublishDomainEvents(expectedDomainEvents - receivedEvent)
      } else {
        shouldPublishDomainEvents(expectedDomainEvents)
      }
    }
  }
}

Thanks!


Solution

  • Solved thanks to Artur Soler :)

    Here you have the solution just in case it helps anyone:

    case class ExpectedDomainEventsOfType[EventType <: DomainEvent, EventId](
      eventAsserter: EventType => Unit,
      eventIdExtractor: EventType => EventId,
      expectedEventIds: Set[EventId]
    )(implicit expectedEventTypeClassTag: ClassTag[EventType]) {
    
      def isOfEventType(event: DomainEvent): Boolean = expectedEventTypeClassTag.runtimeClass.isInstance(event)
    
      def withReceivedEvent(event: DomainEvent): ExpectedDomainEventsOfType[EventType, EventId] = event match {
        case asExpectedEventType: EventType =>
          eventAsserter(asExpectedEventType)
    
          val eventId = eventIdExtractor(asExpectedEventType)
          expectedEventIds should contain(eventId)
    
          copy(expectedEventIds = expectedEventIds - eventId)
      }
    }
    
    def shouldPublishDomainEvents(
      expectedEvents: Set[ExpectedDomainEventsOfType[_, _]]
    )(implicit gotContext: GotContextTest): Unit = {
    
      if (expectedEvents.nonEmpty) {
        val receivedEvent = gotContext.eventBusTestSubscriber.receiveN(1).head.asInstanceOf[DomainEvent]
    
        expectedEvents.find(expectedEventsOfType => expectedEventsOfType.isOfEventType(receivedEvent)) match {
          case Some(expectedEventsOfReceivedType) =>
            val expectedEventsWithoutTheReceived = expectedEventsOfReceivedType.withReceivedEvent(receivedEvent)
    
            if (expectedEventsWithoutTheReceived.expectedEventIds.isEmpty) {
              shouldPublishDomainEvents(expectedEvents - expectedEventsOfReceivedType)
            } else {
              shouldPublishDomainEvents(expectedEvents - expectedEventsOfReceivedType + expectedEventsWithoutTheReceived)
            }
    
          case None =>
            shouldPublishDomainEvents(expectedEvents)
        }
      }
    }