How to mock child Actors for testing an Akka syste

2020-08-10 07:49发布

问题:

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.

回答1:

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")
  }
}


回答2:

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.



回答3:

Maybe this solution will help anyone solve this problem.

I have an parent-actor class which creates some child-actors. Parent-actor acts like a forwarder, it checks if child exists by provided id and sends message to it if so. In parent-actor I use context.child(actorId) to check if child already exist. If I want to test how the parent-actor will behave and what he will send to it's child I use below code:

"ParentActor " should " send XXX message to child actor if he receives YYY message" in {
   val parentActor = createParentActor(testActor, "child_id")
   parentActor ! YYY("test_id")
   expectMsg( XXX )
}

def createParentActor(mockedChild: ActorRef, mockedChildId: String): ParentActor = {
    TestActorRef( new ParentActor(){
      override def preStart(): Unit = {
        context.actorOf( Props(new Forwarder(mockedChild)), mockedChildId)
      }
    } )
  }

  class Forwarder(target: ActorRef) extends Actor {
    def receive = {
      case msg => target forward msg
    }
  }


回答4:

You may want to check this solution I found online (credits goes to Stig Brautaset): http://www.superloopy.io/articles/2013/injecting-akka-testprobe.html

It's an elegant solution, but a bit complicated. It starts with creating anotherActor via a trait (ChildrenProvider), than you can have a productionChildrenProvider which returns an AnotherActor instance. While in test, a testChildrenProvider will return a TestProbe instead. Looking at the test code, it's pretty clean. But Actor implementation is something I have to think about.



回答5:

By Akka Documentation it's not recommend to use TestActorRef. There are several approaches you could use instead. One of them is to externalize child creation from parent.

You would need to change your TopActor code, so that it uses creator function instead of directly instancing anotherActor:

class TopActor(anotherActorMaker: ActorRefFactory ⇒ ActorRef) extends Actor {
    val anotherActor = anotherActorMaker(context)

    def receive = {
        case "call another actor" => anotherActor ! "hello"
    }
}

AnotherActor should stay the same:

class AnotherActor extends Actor {

  override def receive = {
    case "hello" => // do some stuff
  }

}

Now, in your test you would use TestProbe to test the message that should be sent to AnotherActor i.e. TestProbe will act as AnotherAction from TopActors perspective:

class TopActorSpec extends MyActorTestSuiteTrait {
    it should "say hello to AnotherActor when receive 'call another actor'" {
        val testProbe = TestProbe()

        // test maker function
        val maker = (_: ActorRefFactory) ⇒ testProbe.ref
        val testTopActor = system.actorOf(Props(new TopActor(maker)))

        testProbe.send(testTopActor, "call another actor")
        testProbe.expectMsg("hello")
    }
}

Of course, in real application we would use maker function that will give us AnotherActor reference instead of TestProbe:

val maker = (f: ActorRefFactory) ⇒ f.actorOf(Props(new AnotherActor))
val parent = system.actorOf(Props(new TopActor(maker)))