When I have a parent actor in Akka, that directly creates a child actor on initialisation, when I want to write unit tests for the parent actor, how can I replace the child actor with a TestProbe or a mock?
For example, with the following contrived code sample:
class TopActor extends Actor {
val anotherActor = context.actorOf(AnotherActor.props, "anotherActor")
override def receive: Receive = {
case "call another actor" => anotherActor ! "hello"
}
}
class AnotherActor extends Actor {
override def recieve: Receive = {
case "hello" => // do some stuff
}
}
If I want to write a test for TopActor, to check the message sent to AnotherActor is "hello", how do I replace the implementation of AnotherActor? It seems like TopActor creates this child directly so this is not easy to access.
The following approach seems to work but overriding the val of anotherActor directly seems a little crude. I was wondering if there were any other cleaner/ recommended solutions, which is why I still asked the question even though I have this working answer:
class TopActorSpec extends MyActorTestSuiteTrait {
it should "say hello to AnotherActor when receive 'call another actor'" {
val testProbe = TestProbe()
val testTopActor = TestActorRef(Props(new TopActor {
override val anotherActor = testProbe.ref
}))
testTopActor ! "call another actor"
testProbe.expectMsg(500 millis, "hello")
}
}
I am pretty new to Scala myself. Nevertheless I faced the same issue and approached it as follows. The idea behind my approach is to inject the information how to spawn a child actor into the corresponding parent. To ensure a clean initialization I create a factory method which I use to instanciate the actor itself:
object Parent {
def props() :Props {
val childSpawner = {
(context :ActorContext) => context.actorOf(Child.props())
}
Props(classOf[Parent], spawnChild)
}
}
class Parent(childSpawner: (ActorContext) => ActorRef) extends Actor {
val childActor = childSpawner(context)
context.watch(childActor)
def receive = {
// Whatever
}
}
object Child {
def props() = { Props(classOf[Child]) }
}
class Child extends Actor {
// Definition of Child
}
Then you can test it like this:
// This returns a new actor spawning function regarding the FakeChild
object FakeChildSpawner{
def spawn(probe :ActorRef) = {
(context: ActorContext) => {
context.actorOf(Props(new FakeChild(probe)))
}
}
}
// Fake Child forewarding messages to TestProbe
class FakeChild(probeRef :ActorRef) extends Actor {
def receive = {
case msg => probeRef ! (msg)
}
}
"trigger actions of it's children" in {
val probe = TestProbe()
// Replace logic to spawn Child by logic to spawn FakeChild
val actorRef = TestActorRef(
new Parent(FakeChildSpawner.spawn(probe.ref))
)
val expectedForewardedMessage = "expected message to child"
actorRef ! "message to parent"
probe.expectMsg("expected message to child")
}
By doing this you extract the spawning action from the parent into an anonymous function which can inside the tests be replaced by the FakeChild actor which is completly in your hands. Forewarding messages from the FakeChild to the TestProbe solves your testing issue.
I hope that helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With