はじめに
この記事では、AkkaのActorを用いたプログラムの単体テスト方法について簡単に説明します。
Actorをテストするのは通常のオブジェクトをテストするよりも下記の理由により困難です。
-
タイミング
メッセージは非同期に送信されるので、単体テストでいつ検査を実行したらよいかを知ることは難しいことです。 -
非同期
Actorは並列のスレッドで実行されます。これはシングルスレッドで実行されるプログラムよりも検査するのが難しいです。 -
ステートレス
Actorは内部の状態を隠蔽しています。通常この内部状態に直接アクセスすることはできないので、単体テストが困難になります。 -
強調と結合
複数のActorと結合して実行されます。これをテストするにはActor間のメッセージのやりとりを盗み聞きする必要があります。
この面倒なテストを、Akka-TestKitを使うことによって軽減します。
Akka-TestKit
Akka-TestKitによって下記のテストが可能になります。
-
シングルスレッド単体テスト
Actorインスタンスは通常直接アクセスできません。testkitはTestActorRefを提供していて、Actorインスタンスへのアクセスができるようになります。これによって、Actorに定義されているメソッドを呼びだすことができます。シングルスレッドで実行することにより、通常のオブジェクトと同様にテストを記述できます。 -
マルチスレッド単体テスト
testkitは同様に、TestKitと、TestProbeクラスを提供しています。TestProbeを使うと、Actorからの返信をインスペクトすることができます。TestKitクラスは、期待しているメッセージかどうかを確かめることができます。Actorは通常のマルチスレッド環境で実行されます。 -
複数JVMテスト
Akkaは同様に複数のJVMのテストも可能にします。これは、リモートのアクターシステムをより簡単にテストできるようにする仕組みです。
TestActorRef
TestActorRefは同期テストを可能にします、通常Actorのインスタンスは取得できませんが、TestActorRefを用いるとインスタンスが取得できます。
import akka.testkit.TestActorRef
val actorRef = TestActorRef[MyActor]
val actor = actorRef.underlyingActor
サンプルコード
必要インポート
import org.scalatest.{ Suite, BeforeAndAfterAll, WordSpecLike, MustMatchers }
import akka.testkit.{ TestKit, TestActorRef }
import akka.actor.{ Props, ActorSystem, Actor, ActorRef }
アクターシステムをテスト後に停止するトレイト
SBTなどでテストを実行すると、アクターは動作しつづけてしまうので、テスト後に停止するようにします。
trait StopSystemAfterAll extends BeforeAndAfterAll {
this: TestKit with Suite =>
override protected def afterAll() {
super.afterAll()
system.shutdown()
}
}
プロトコル
ActorではActorとのメッセージのやりとりをプロトコルとしてグループ化しておく習慣があるようです。
object ActorProtocol {
case class Message(data: String)
case class GetState(receiver: ActorRef)
}
アクター
varを使っていますが、アクターは同時に動作することはないのでロックなどは気にしなくても問題ありません。
class MessageActor extends Actor {
import ActorProtocol._
var internalState = Vector[String]()
def receive = {
case Message(data) => internalState = internalState :+ data
case GetState(receiver) => receiver ! internalState
}
def state = internalState
}
シングルスレッドのスペック
TestKitをextendsすることにより、testActorやexpectMsgアサーションなどの機能が使えるようになります。
class ActorSpec extends TestKit(ActorSystem("test"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
"A actor" must {
"change state when it receives a message" in {
import ActorProtocol._
val actor = TestActorRef[MessageActor]
actor ! Message("ハロー")
actor.underlyingActor.state must (contain("ハロー"))
}
}
}
マルチスレッドのスペック
testActorをアクターに渡すことにより、testActorで受け取ったメッセージを、expectMsgで確認することができるようになります。
class ActorSpec extends TestKit(ActorSystem("test"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
"A actor" must {
"change state when it receives a message, multi threaded" in {
import ActorProtocol._
val actor = system.actorOf(Props[MessageActor], "s3")
actor ! Message("ハロー")
actor ! Message("はろ〜")
actor ! GetState(testActor)
expectMsg(Vector("ハロー", "はろ〜"))
}
}
}
送信されたメッセージを取得する
val message = receiveOne(1 second)
リスナーでメッセージを受け取る
class Sender(listener: Option[ActorRef] = None) extends Actor {
def receive = {
case Send(msg) =>
listener.foreach { _ ! msg }
}
}
val sender = system.actorOf(Props(Sender(testActor)))
sender ! Message("test")
receiveOne(1 second)
おわりに
TestKitでは、expectMsgの様なアクターをテストするための便利なメソッドが多数用意されていますので詳しいことはScalaDocを参照して下さい。http://doc.akka.io/api/akka/2.0/akka/testkit/TestKit.html