読者です 読者をやめる 読者になる 読者になる

KZKY memo

自分用メモ.

怒涛の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
        }
    }
}