Search code examples
scalatestingplayframeworkakkaactor

Play! scala and Akka: how to test if an actor A sent a message to an actor B?


I want to test that an actor A send a message to an actor B after have received a message.

I'm using Play! 2.5 and I use the factories since I need to inject some of my classes and things like wSClient inside the actors.

The Actor A looks like:

object ActorA {
  trait Factory {
    def apply(ec: ExecutionContext, actorBRef: ActorRef): Actor
  }
}

class ActorA @Inject()(implicit val ec: ExecutionContext,
                       @Named("actor-b") actorBRef: ActorRef)
    extends Actor with ActorLogging with InjectedActorSupport {

  override def receive: Receive = {
    case i: Long =>
      log info s"received $i"
      actorBRef ! (i+1)
}

And the actor B is even more simple:

object ActorB {
  trait Factory {
    def apply(): Actor
  }
}

class ActorB extends Actor with ActorLogging {

  override def receive: Receive = {
    case _ =>
      log error "B received an unhandled message"
  }
}

But my test doesn't pass, it is said that the expected message doesn't arrive, I get a Timeout in the test (but it is well logged by the actor B) so the problem comes from the test (and probably the Probe).

Here is the test:

  val actorBProbe = TestProbe()
  lazy val appBuilder = new GuiceApplicationBuilder().in(Mode.Test)
  lazy val injector = appBuilder.injector()
  lazy val factory = injector.instanceOf[ActorA.Factory]
  lazy val ec = scala.concurrent.ExecutionContext.Implicits.global
  lazy val factoryProps = Props(factory(ec, actorBProbe.ref))
  val ActorARef = TestActorRef[ActorA](factoryProps)

  "Actor B" must {

    "received a message from actor A" in {
      ActorARef ! 5L

      actorBProbe.expectMsg(6L)
    }
  }

I also created a minimum Play! application with the code above available here.


Solution

  • In your test, actorBProbe is not the ActorB ref passed to ActorA constructor (of ref ActorARef). What really happens is that Guice creates a different ActorB (named actor-b), and passes its ref to ActorA (of ref ActorARef) constructor.

    The test ends up with ActorB actor-b receiving 6L (as evident in log). While actorBProbe receives nothing.

    The confusion really comes from mixing Guice lifecyle with Actors. In my experience, it creates more pains than I can bear.

    To prove, simply print hash code of ActorRef's, you'll see they are different. Illustrated as followings:

    val actorBProbe = TestProbe()
    println("actorBProbe with ref hash: " + actorBProbe.ref.hashCode())
    

    And,

    class ActorA ... {
      override def preStart =
        log error "preStart actorBRef: " + actorBRef.hashCode()
    
      // ...
    }
    

    In fact, even ec inside ActorA is not the same ec in the test code.

    The following is a way to "force" the test to pass and at the same time prove that actorBProbe wasn't really being used by ActorB.

    In stead of relying on Guice to "wire in" ActorB, we tell Guice to leave it alone by replacing @Named("actor-b") with @Assisted, like this,

    import ...
    import com.google.inject.assistedinject.Assisted
    
    class ActorA @Inject()(...
      /*@Named("actor-b")*/ @Assisted actorBRef: ActorRef)
    ...
    

    Re-run the test, it'll pass. But this is probably not what you wanted to begin with.