如何模拟儿童演员测试Akka系统?

时间:2015-10-18 10:33:16

标签: scala unit-testing akka

当我在Akka中有一个父actor时,在初始化时直接创建一个子actor,当我想为父actor编写单元测试时,如何用TestProbe或mock替换子actor?

例如,使用以下设计的代码示例:

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
  }

}

如果我想为TopActor编写测试,要检查发送给AnotherActor的消息是“hello”,我该如何替换AnotherActor的实现?看起来TopActor直接创建了这个孩子,因此不容易访问。

5 个答案:

答案 0 :(得分:9)

以下方法似乎有效但直接覆盖anotherActor的val似乎有点粗糙。我想知道是否有其他更清洁/推荐的解决方案,这就是为什么我仍然问这个问题,即使我有这个有效的答案:

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

答案 1 :(得分:1)

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.

答案 2 :(得分:1)

也许这个解决方案可以帮助任何人解决这个问题。

我有一个父级演员类,可以创建一些子演员。 Parent-actor的作用类似于转发器,它通过提供的id检查子进程是否存在,如果是,则向其发送消息。在父级角色中,我使用context.child(actorId)来检查孩子是否已经存在。如果我想测试父 - 演员将如何表现以及他将如何发送给它的孩子我使用下面的代码:

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

答案 3 :(得分:0)

您可能想查看我在网上找到的这个解决方案(积分转到 Stig Brautaset ): http://www.superloopy.io/articles/2013/injecting-akka-testprobe.html

这是一个优雅的解决方案,但有点复杂。它首先通过trait(ChildrenProvider)创建anotherActor,而不是生成一个返回AnotherActor实例的productionChildrenProvider。在测试中,testChildrenProvider将返回TestProbe。 看一下测试代码,它非常干净。但是演员的实施是我必须要考虑的事情。

答案 4 :(得分:0)

通过Akka Documentation,不建议使用TestActorRef。您可以使用several approaches代替。其中之一是去externalize child creation from parent

您需要更改您的TopActor代码,以便它使用创建者函数而不是直接实例化anotherActor

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

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

另一个演员应该保持不变:

class AnotherActor extends Actor {

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

}

现在,在您的测试中,您将使用TestProbe来测试应该发送给AnotherActor的消息,即,从TopActors角度来看,TestProbe将充当AnotherAction:

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

当然,在实际应用中,我们将使用maker函数,该函数将为我们提供AnotherActor参考,而不是TestProbe:

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