Wednesday, February 5, 2014

Akka: Testing messages sent to child actors

I previously blogged about testing messages sent back to the sender. Another common scenario is testing that an actor sends the correct message to a child. A piece of code that does this might look like:

class ParentActor extends Actor {
def receive: Actor.Receive = {
case msg @ _ => {
println(s"Received msg, delegating work to a child actor")
val childActor = context.actorOf(Props[ChildActor])
childActor ! "Go and do something important!"
}
}
}
But how do you test drive this kind of code?

class ParentActorTest extends TestKit(ActorSystem("TestSystem")) with FunSuiteLike {
test("Should delegate the important work to the client") {
val underTest = TestActorRef(new ParentActor)
underTest ! "Go do some work"
// how do i test this?
}
}
I've adopted the approach of removing the responsibility of creating the child from the ParentActor. To do this you can pass in a factory to the ParentActor's constructor that can be replaced for testing. To do this the ParentActor is changed to look like this:

class ParentActor(childFactory: (ActorRefFactory) => ActorRef) extends Actor {
def receive: Actor.Receive = {
case msg @ _ => {
println(s"Received msg, delegating work to a child actor")
val childActor = childFactory(context)
childActor ! "Go and do something important!"
}
}
}
The ParentActor now takes in a factory function which takes in an ActorFactoryRef and returns a ActorRef. This function is responsible for creating the ChildActor.

childFactory: (ActorRefFactory) => ActorRef

This is then used by the ParentActor as follows:

val childActor = childFactory(context)

You can now inject a TestProbe for testing rather than using a real ActorFactorRef. For example:

class ParentActorTest extends TestKit(ActorSystem("TestSystem")) with FunSuiteLike {
test("Should delegate the important work to the client") {
val testProbeForChild = TestProbe()
val underTest = TestActorRef(new ParentActor(_ => testProbeForChild.ref))
underTest ! "Go do some work"
testProbeForChild.expectMsg("Go and do something important!")
}
}
And for the real code you can pass in an anonymous function that uses the ActorRefFactory to actually create a real ChildActor:

test("Using a real child actor") {
val underTest = TestActorRef(new ParentActor(actorFactory => actorFactory.actorOf(Props[ChildActor])))
underTest ! "Go do some work"
// Can't test this but shows how to crate a ParentActor in production code
}
Here you can see the ActorFactoryRef (context in Actors implement this interface) is used to create an Actor the same way as the original ParentActor did.

Full source code here.

1 comment:

Ratman said...

Hi, nice job with this scenario. I have a question about parent actor: you can easily create an actor with "new", but how to create a parent actor ref? With actor instance I cannot use ! method.

Thanks