如何模拟子 Actors 来测试 Akka 系统?
How to mock child Actors for testing an Akka system?
当我在 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 直接创建了这个 child,所以这不容易访问。
下面的方法似乎可行,但直接覆盖 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")
}
}
您可能需要查看我在网上找到的这个解决方案(感谢 Stig Brautaset):
http://www.superloopy.io/articles/2013/injecting-akka-testprobe.html
这是一个优雅的解决方案,但有点复杂。首先是通过特征 (ChildrenProvider) 创建另一个 Actor,然后您可以拥有一个 return 是 AnotherActor 实例的 productionChildrenProvider。在测试中,testChildrenProvider 将 return 改为 TestProbe。
查看测试代码,它很干净。但是Actor的实现是我必须要考虑的事情。
我自己对 Scala 还是很陌生。尽管如此,我还是遇到了同样的问题,并按如下方式处理。我的方法背后的想法是将如何生成子 actor 的信息注入到相应的父 actor 中。为了确保干净的初始化,我创建了一个工厂方法,我用它来实例化 actor 本身:
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
}
然后你可以这样测试:
// 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")
}
通过这样做,您可以将生成动作从父级提取到一个匿名函数中,该函数可以在测试中由完全掌握在您手中的 FakeChild actor 替换。将消息从 FakeChild 转发到 TestProbe 可以解决您的测试问题。
希望对您有所帮助。
也许这个解决方案可以帮助任何人解决这个问题。
我有一个 parent-actor class,它创建了一些 child-actors。 Parent-actor 就像转发器一样,它通过提供的 id 检查 child 是否存在,如果存在则向它发送消息。在 parent-actor 中,我使用 context.child(actorId)
来检查 child 是否已经存在。如果我想测试 parent-actor 的行为方式以及他将发送给 child 的内容,我使用以下代码:
"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
}
}
来自 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。
您需要更改您的 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 的消息,即 TestProbe 将从 TopActor 的角度充当 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)))
当我在 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 直接创建了这个 child,所以这不容易访问。
下面的方法似乎可行,但直接覆盖 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")
}
}
您可能需要查看我在网上找到的这个解决方案(感谢 Stig Brautaset): http://www.superloopy.io/articles/2013/injecting-akka-testprobe.html
这是一个优雅的解决方案,但有点复杂。首先是通过特征 (ChildrenProvider) 创建另一个 Actor,然后您可以拥有一个 return 是 AnotherActor 实例的 productionChildrenProvider。在测试中,testChildrenProvider 将 return 改为 TestProbe。 查看测试代码,它很干净。但是Actor的实现是我必须要考虑的事情。
我自己对 Scala 还是很陌生。尽管如此,我还是遇到了同样的问题,并按如下方式处理。我的方法背后的想法是将如何生成子 actor 的信息注入到相应的父 actor 中。为了确保干净的初始化,我创建了一个工厂方法,我用它来实例化 actor 本身:
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
}
然后你可以这样测试:
// 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")
}
通过这样做,您可以将生成动作从父级提取到一个匿名函数中,该函数可以在测试中由完全掌握在您手中的 FakeChild actor 替换。将消息从 FakeChild 转发到 TestProbe 可以解决您的测试问题。
希望对您有所帮助。
也许这个解决方案可以帮助任何人解决这个问题。
我有一个 parent-actor class,它创建了一些 child-actors。 Parent-actor 就像转发器一样,它通过提供的 id 检查 child 是否存在,如果存在则向它发送消息。在 parent-actor 中,我使用 context.child(actorId)
来检查 child 是否已经存在。如果我想测试 parent-actor 的行为方式以及他将发送给 child 的内容,我使用以下代码:
"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
}
}
来自 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。
您需要更改您的 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 的消息,即 TestProbe 将从 TopActor 的角度充当 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)))