如何模拟子Actor来测试Akka系统?

14

在Akka中,如果我有一个父Actor,在初始化时直接创建了一个子Actor,当我想为父Actor编写单元测试时,如何使用一个TestProbe或模拟对象替换子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个回答

11
以下方法似乎可以工作,但直接覆盖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")
  }
}

由于这个解决方案没有其他答案并且我得到了一个赞,我想我会接受自己的答案 :) - sean_robbins
1
你的 testTopActor.underlyingActor 将会同时拥有 anotherActorTopActor.anotherActor。如果你的 anotherActor 在其构造函数或任何生命周期函数中没有执行任何操作,那么这可能是可以接受的。但是,如果在构造函数中有一些运行,例如网络/数据库连接(我知道这很糟糕,但只是为了说明问题),那么当你创建你的 testTopActor 时就会有两个这样的操作正在运行。也许要注意这种情况。 - CrazyGreenHand

2
根据Akka文档,不建议使用TestActorRef。有几种替代方案可供选择。其中之一是将子项创建外部化于父项
您需要更改TopActor代码,以便使用creator函数而不是直接实例化anotherActor
class TopActor(anotherActorMaker: ActorRefFactoryActorRef) extends Actor {
    val anotherActor = anotherActorMaker(context)

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

AnotherActor应该保持不变:

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

当然,在实际应用中,我们将使用制造函数来获取AnotherActor引用,而不是TestProbe:
val maker = (f: ActorRefFactory) ⇒ f.actorOf(Props(new AnotherActor))
val parent = system.actorOf(Props(new TopActor(maker)))

1

我自己对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 替换它,该 actor 完全由您控制。将来自 FakeChild 的消息转发到 TestProbe 可以解决您的测试问题。
希望这可以帮助您。

1
也许这个解决方案能帮助任何人解决这个问题。
我有一个父actor类,它创建了一些子actor。父actor充当转发器的角色,它通过提供的id检查子actor是否存在,如果存在则向其发送消息。在父actor中,我使用context.child(actorId)来检查子actor是否已经存在。如果我想测试父actor的行为以及它将向其子actor发送什么消息,我会使用以下代码:
"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
    }
  }

0

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

这是一个优雅的解决方案,但有点复杂。它通过一个trait(ChildrenProvider)创建了另一个Actor,然后你可以有一个返回AnotherActor实例的productionChildrenProvider。而在测试中,testChildrenProvider将返回一个TestProbe。 看一下测试代码,它非常干净。但是Actor的实现是我必须考虑的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接