怒涛のAkka: Testing Actor Systems
基本
Synchronous/Asynchronousの2つがあると考えて良い.
Synchronous Test
メッセージの順序が決定的かつコンカレンシーを考えない場合のテスト
基本は
import akka.testkit.TestActorRef import scala.concurrent.duration._ import scala.concurrent.Await import akka.pattern.ask val actorRef = TestActorRef(new MyActor) // hypothetical message stimulating a ’42’ answer val future = actorRef ? Say42 val Success(result: Int) = future.value.get result should be(42)
な感じで,テストしたいアクターをTestActorRefでラップする.
そうするとdispatcherをCallingThreadDispatcherに,receiveTimeoutをNoneにしてくれる.
FSMの場合は,TestFSMRefが用意されているのでそれを使えば良い.
Asynchronous Test
Actorが普通の特性, concurrent, non-deterministic w.r.t. ordering, and at-least-one messageを持っているの場合のテスト.
基本はこのように書く.
- class MySpec ecextends TestKitを中心にいろいろwithする
- BDD-styleでbehavioursを書く
- systemは最後にshutdownすること
import akka.actor.ActorSystem import akka.actor.Actor import akka.actor.Props import akka.testkit.{ TestActors, TestKit, ImplicitSender } import org.scalatest.WordSpecLike import org.scalatest.Matchers import org.scalatest.BeforeAndAfterAll class MySpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll { def this() = this(ActorSystem("MySpec")) override def afterAll { TestKit.shutdownActorSystem(system) } "An Echo actor" must { "send back messages unchanged" in { val echo = system.actorOf(TestActors.echoActorProps) echo ! "hello world" expectMsg("hello world") } } _system.shutdown() }
Built-in Assertion
いっぱいあるので,Doc参照.
基本はこれ
expectMsg[T](d: Duration, msg: T): T
EventFilter
EventFilterを使うときは,
- reference.conf
akka.loggers = [akka.testkit.TestEventListener]
か
- code
ActorSystem("testsystem", ConfigFactory.parseString(""" akka.loggers = ["akka.testkit.TestEventListener"] """))
をしないと,intercept blockから先に進まないので注意.
Timing Assertions
- code
within([min, ]max) { ... }
な感じでwithinに渡されるblockの実行時間の下限,上限を設定可能.
expectNoMsg or receiveWhileを設定するとmax時間チェックが行われないが,
expectNoMsg or receiveWhileまではmax時間チェックは行われる.block内の実行以外のoverhead(e.g., wake-up)をmax時間チェック入れたくない場合に使う.
時間チェックはCPU時間でなくて一般的な時間なことに注意.
ここに良い例がある
Using Multiple Probe Actors
TestKitを使っているとmessagesがどういう順で来たかとか基本わからない.
なので,TestProbeを使う.
一度ActorにProbeActorを送って,そいつから返事を返してもらう例
- code snippet
import scala.concurrent.duration._ import akka.actor._ import scala.concurrent.Future class MyDoubleEcho extends Actor { var dest1: ActorRef = _ var dest2: ActorRef = _ def receive = { case (d1: ActorRef, d2: ActorRef) => dest1 = d1 dest2 = d2 case x => dest1 ! x dest2 ! x } } val probe1 = TestProbe() val probe2 = TestProbe() val actor = system.actorOf(Props[MyDoubleEcho]) actor ! ((probe1.ref, probe2.ref)) actor ! "hello" probe1.expectMsg(500 millis, "hello") probe2.expectMsg(500 millis, "hello")
TestProbe内でのカスタムアサーション可能
final case class Update(id: Int, value: String) val probe = new TestProbe(system) { def expectUpdate(x: Int) = { expectMsgPF() { case Update(id, _) if id == x => true } sender() ! "ACK" } }
警告
CallingTreadDispathcerを使っているActorとTestProbeで互いにmessageを送っていたり,TestProbe.watch/TestProbe.unwatch をしているとdeal-lockする可能性ありなので警告.すなわち,TestProbeの中でTestActorRefをwatchしていると危険.
Watching Other Actors from Probes
他のActorのDeatchWatchが可能.
val probe = TestProbe()
probe watch target
target ! PoisonPill
probe.expectTerminated(target)
Replying to Messages Received by Probes
TestProbeがメッセージを受け取って返すこともできる.
val probe = TestProbe() val future = probe.ref ? "hello" probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher probe.reply("world") assert(future.isCompleted && future.value == Some(Success("world")))
Forwarding Messages Received by Probes
TestProbeは,forward proxyのような使い型もできる.
- Actors
class Source(target: ActorRef) extends Actor { def receive = { case "start" => target ! "work" } } class Destination extends Actor { def receive = { case x => // Do something.. } }
- Test Code
val probe = TestProbe() val source = system.actorOf(Props(classOf[Source], probe.ref)) val dest = system.actorOf(Props[Destination]) source ! "start" probe.expectMsg("work") probe.forward(dest)
Auto-Pilot
メッセージのforwardingをAuto-Pilotで可能.
val probe = TestProbe() probe.setAutoPilot(new TestActor.AutoPilot { def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { case "stop" => TestActor.NoAutoPilot case x => testActor.tell(x, sender); TestActor.KeepRunning } })
な感じでAutoPilotを設定.
(Docからでは使い方よくわからないのに,検証すらしてないので注意)
Testing parent-child relationships
Actorの中でActorを作ると,parent-childの関係が生まれるので,それらはcouplingしている.テスタビリティ的に良くない.
この状況で,テスタビリティを向上させるアプローチ
- childを作る際に,parentの参照を明示的に与える
- parentを作る際に,childのファクトリメソッドを与える設計にする
- テストの際に擬似的なparentを作る
Docでは選択肢2がいいと言っているが,別に選択肢3でもこと足りると言ってる.
詳しくはDoc見る.
CallingThreadDispatcher
これをテストで使う利点が3つある
- Actorぽくテストできるし,シングルスレなので決定的にテストできる
- 例外のスタックトレイスがゲットできてどこでエラーしているか突き止めやすい
- dead-lockシナリオの回避につながる
Tracing Actor Invocations
Full loggingする設定
akka {
loglevel = "DEBUG"
actor {
debug {
receive = on
autoreceive = on
lifecycle = on
}
}
}